diff --git a/.vscode/settings.json b/.vscode/settings.json index 6fa3cc4..646383b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -52,6 +52,8 @@ "Typica", "Verte", "VSOP", + "wineyard", + "wineyards", "Yamahai", "Yuzu" ] diff --git a/src/models/wine.ts b/src/models/wine.ts index 8df38df..ac967f4 100644 --- a/src/models/wine.ts +++ b/src/models/wine.ts @@ -4,7 +4,9 @@ import { Viticulture, BottleClosure, VintageOptions, - StandardDrinks100ml + StandardDrinks, + WineRegion, + WineVolume } from '../types' import { Alpha2Code } from 'i18n-iso-countries' import { CurrencyCode } from 'currency-codes-ts/dist/types' @@ -18,13 +20,14 @@ export class Wine { public style: string, // bubbles+fizz, table, dessert, fortified, vermouth public characteristic: string, // light aromatic, textural, fruit forward, structural & savoury, powerful public country: Alpha2Code, // two-letter country codes defined in ISO 3166-1 (https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) - public region: string, // appellation, village, sub-region, vineyard + public region: WineRegion, // appellation, village, sub-region, vineyard public name: string, // label public producerId: ObjectId, // product producer public varietal: string, // if more than one, list as 'blend' public vintage: number | VintageOptions, // year, nv (non-vintage) or mv (multi-vintage) + public volume: WineVolume, // bottle volume public alcohol: number, // alcohol percentage - public standardDrinks100ml: StandardDrinks100ml, // an amount of standard drinks per 100ml in AU, UK and US + public standardDrinks: StandardDrinks, // an amount of standard drinks per 100ml and bottle in AU, UK and US public viticulture: Viticulture, // viticulture public sulfites: number, // parts per million public filtered: boolean, // is wine filtered (fined (egg or fish)) diff --git a/src/routes/wines.router.ts b/src/routes/wines.router.ts index b7ca3f3..9c032da 100644 --- a/src/routes/wines.router.ts +++ b/src/routes/wines.router.ts @@ -5,7 +5,8 @@ import { wineValidation, handleReqError, handleReqSuccess, - alcoholToStandardDrinks + alcoholToStandardDrinks, + volumeToMl } from '../utils' import Joi from 'joi' @@ -76,7 +77,10 @@ winesRouter.post('/', async (req: Request, res: Response) => { } } - wine.standardDrinks100ml = alcoholToStandardDrinks(wine.alcohol) + wine.standardDrinks = alcoholToStandardDrinks( + wine.alcohol, + volumeToMl(wine.volume) + ) const result = await collections.wines?.insertOne(wine) diff --git a/src/types/product.ts b/src/types/product.ts index d562f86..76949fd 100644 --- a/src/types/product.ts +++ b/src/types/product.ts @@ -66,8 +66,7 @@ export enum VintageOptions { MV = 'mv' } -export interface StandardDrinks100ml { - AU: number - UK: number - US: number +export interface StandardDrinks { + '100ml': { AU: number; UK: number; US: number } + bottle: { AU: number; UK: number; US: number } } diff --git a/src/types/wine.ts b/src/types/wine.ts index c26bccd..f5e490b 100644 --- a/src/types/wine.ts +++ b/src/types/wine.ts @@ -16,3 +16,21 @@ export enum BottleClosure { CrownSeal = 'crown-seal', Screwcap = 'screwcap' } + +export interface WineRegion { + [key: string]: + | string[] + | { [key: string]: string[] | { [key: string]: string[] } } +} + +export enum WineVolume { + '0.05L' = '0.05L', + '0.187L' = '0.187L', + '0.375L' = '0.375L', + '0.5L' = '0.5L', + '0.75L' = '0.75L', + '1.5L' = '1.5L', + '3L' = '3L', + '6L' = '6L', + '12L' = '12L' +} diff --git a/src/utils/alcohol.ts b/src/utils/alcohol.ts index 3cf3f3a..05d4ed2 100644 --- a/src/utils/alcohol.ts +++ b/src/utils/alcohol.ts @@ -1,10 +1,36 @@ -import { StandardDrinks100ml } from '../types' +import { StandardDrinks, WineVolume } from '../types' import { roundToOneDecimal } from './' export const alcoholToStandardDrinks = ( - alcohol: number -): StandardDrinks100ml => ({ - UK: roundToOneDecimal(10 * alcohol), - AU: roundToOneDecimal(7.91 * alcohol), - US: roundToOneDecimal(5.64 * alcohol) -}) + alcohol: number, + bottle: number +): StandardDrinks => { + const UK100ml = roundToOneDecimal(10 * alcohol) + const AU100ml = roundToOneDecimal(7.91 * alcohol) + const US100ml = roundToOneDecimal(5.64 * alcohol) + + const bottleMultiplier = bottle / 100 + + return { + '100ml': { + UK: UK100ml, + AU: AU100ml, + US: US100ml + }, + bottle: { + UK: UK100ml * bottleMultiplier, + AU: AU100ml * bottleMultiplier, + US: US100ml * bottleMultiplier + } + } +} + +export const volumeToMl = (volume: WineVolume): number => { + if (volume.endsWith('L')) { + const volumeMl = volume.replace('L', '') + + return Number(volumeMl) * 1000 + } + + throw new Error('Not supported volume type') +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 8455920..281f49a 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -3,3 +3,4 @@ export * from './nostr' export * from './route' export * from './utils' export * from './alcohol' +export * from './wine' diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 5019bf3..ba49996 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,2 +1,5 @@ export const roundToOneDecimal = (number: number) => Math.round(number * 10) / 10 + +export const isObject = (item: unknown) => + typeof item === 'object' && !Array.isArray(item) && item !== null diff --git a/src/utils/validation/wine.ts b/src/utils/validation/wine.ts index fa90e9e..5a61c40 100644 --- a/src/utils/validation/wine.ts +++ b/src/utils/validation/wine.ts @@ -3,8 +3,11 @@ import { WineType, VintageOptions, BottleClosure, - Viticulture + Viticulture, + WineRegion, + WineVolume } from '../../types' +import { wineRegionsMap, isObject } from '../' export const wineValidation = (data: unknown): Joi.ValidationResult => Joi.object({ @@ -17,18 +20,256 @@ export const wineValidation = (data: unknown): Joi.ValidationResult => style: Joi.string().required(), characteristic: Joi.string().required(), country: Joi.string().length(2), - region: Joi.string().required(), + region: Joi.alternatives() + .try( + Joi.string().custom((value, helper) => { + if (value) { + if (helper.state.ancestors) { + const { country } = helper.state.ancestors[0] + + if (country) { + const regionMap = wineRegionsMap[country] + + if (regionMap) { + const regions = Array.isArray(regionMap) + ? regionMap + : Object.keys(regionMap) + + if (!regions.includes(value)) { + return helper.message({ + custom: Joi.expression( + `"region" contains an invalid value. Valid values for ${country} are [${regions.join(', ')}]` + ) + }) + } + } + } + } + } + + return value + }), + Joi.object().custom( + ( + value: { + [key: string]: + | string + | { [key: string]: string | { [key: string]: string } } + }, + helper: Joi.CustomHelpers<unknown> + ) => { + // return if no value + if (!value) { + return value + } + // return if no state ancestors + if (!helper.state.ancestors) { + return value + } + + const country: string = helper.state.ancestors[0].country + + // return if no country + if (!country) { + return value + } + + const regionMap = wineRegionsMap[country] + + // return if no region map + if (!regionMap) { + return value + } + + /** + * Region + */ + // list of supported regions + const regions = Array.isArray(regionMap) + ? regionMap + : Object.keys(regionMap) + + const providedRegions = Object.keys(value) + + // check if multiple regions provided + if (providedRegions.length !== 1) { + return helper.message({ + custom: Joi.expression( + `"region" contains an invalid value. Valid values a single string or a region object` + ) + }) + } + + const returnCustomMessage = ( + map: string[], + options: string[] + ): Joi.ErrorReport => + helper.message({ + custom: Joi.expression( + `"region" contains an invalid value. Valid values for ${map.join(' -> ')} are [${options.join(', ')}]` + ) + }) + + const providedRegion = providedRegions[0] + + // check if provided region is in list of supported regions + if (!regions.includes(providedRegion)) { + return returnCustomMessage([country], regions) + } + + /** + * Subregion + */ + // list of supported subregions + const subRegions: string[] = Object.keys( + (regionMap as WineRegion)[providedRegion] + ) + + const providedSubRegion: + | string + | { [key: string]: string | { [key: string]: string } } = + value[providedRegion] + + // return if provided subregion is not a string or an object + if ( + typeof providedSubRegion !== 'string' && + !isObject(providedSubRegion) + ) { + return returnCustomMessage([country, providedRegion], subRegions) + } + + // if providedSubRegion is a string, check if it is in the list of supported subregions + if ( + typeof providedSubRegion === 'string' && + !subRegions.includes(providedSubRegion) + ) { + return returnCustomMessage([country, providedRegion], subRegions) + } + // if providedSubRegion is an object, check if it is in the list of supported subregions + else if (isObject(providedSubRegion)) { + const providedSubRegions = Object.keys(providedSubRegion) + const providedSubRegionName: string = providedSubRegions[0] + + // return if provided multiple subregions or if provided subregion is not in the supported list + if ( + providedSubRegions.length !== 1 || + !subRegions.includes(providedSubRegionName) + ) { + return returnCustomMessage( + [country, providedRegion], + subRegions + ) + } + + /** + * Village + */ + // list of supported villages + const villages: string[] = Object.keys( + ( + (regionMap as WineRegion)[providedRegion] as { + [key: string]: string | { [key: string]: string[] } + } + )[providedSubRegionName] + ) + + const providedVillage: string | { [key: string]: string } = ( + value[providedRegion] as { + [key: string]: string + } + )[providedSubRegionName] + + // return if provided village is not a string or an object + if ( + typeof providedVillage !== 'string' && + !isObject(providedVillage) + ) { + return returnCustomMessage( + [country, providedRegion, providedSubRegionName], + subRegions + ) + } + + // if village is a string, check if it is in the supported list + if ( + typeof providedVillage === 'string' && + !villages.includes(providedVillage) + ) { + return returnCustomMessage( + [country, providedRegion, providedSubRegionName], + villages + ) + } + // if providedVillage is an object, check if it is in the supported list + else if (isObject(providedVillage)) { + const providedVillages = Object.keys(providedVillage) + + const providedVillageName: string = providedVillages[0] + + // return if provided multiple villages or if provided village is not in the supported list + if ( + providedVillages.length !== 1 || + !villages.includes(providedVillageName) + ) { + return returnCustomMessage( + [country, providedRegion, providedSubRegionName], + villages + ) + } + + /** + * Wineyard + */ + // list of supported wineyards + const wineyards: string[] = ( + (regionMap as WineRegion)[providedRegion] as { + [key: string]: { [key: string]: string[] } + } + )[providedSubRegionName][providedVillageName] + + const providedWineyard: string = ( + value[providedRegion] as { + [key: string]: { + [key: string]: string + } + } + )[providedSubRegionName][providedVillageName] + + // check if provided wineyard is in the supported list + if (!wineyards.includes(providedWineyard)) { + return returnCustomMessage( + [ + country, + providedRegion, + providedSubRegionName, + providedVillageName + ], + wineyards + ) + } + } + } + + return value + } + ) + ) + .allow('') + .required(), name: Joi.string().required(), producerId: Joi.string().length(24).required(), varietal: Joi.string().required(), vintage: Joi.alternatives() .try(Joi.string().valid(...Object.values(VintageOptions)), Joi.number()) .required(), + volume: Joi.string() + .valid(...Object.values(WineVolume)) + .required(), alcohol: Joi.number().min(0).max(0.99).required(), viticulture: Joi.string() .valid(...Object.values(Viticulture)) .required(), - sulfites: Joi.number().required(), + sulfites: Joi.number().min(0).max(400), filtered: Joi.boolean().required(), vegan: Joi.boolean().required(), kosher: Joi.boolean().required(), diff --git a/src/utils/wine.ts b/src/utils/wine.ts new file mode 100644 index 0000000..bb6794d --- /dev/null +++ b/src/utils/wine.ts @@ -0,0 +1,689 @@ +import { WineRegion } from '../types' + +export const wineRegionsMap: { [key: string]: string[] | WineRegion } = { + IT: { + Piedmont: { + Langhe: { + Barolo: ['La Morra', 'Castiglione Falletto', `Serralunga d'Alba`], + Barbaresco: ['Neive', 'Alba', 'Treiso'] + }, + Monferrato: ['Gavi', 'Asti', 'Canelli'] + }, + Tuscany: [ + 'Chianti', + 'Montalcino', + 'Montepulciano', + 'Scansano', + 'Carmignano' + ], + Veneto: ['Valpolicella', 'Soave'], + 'Friuli-Venezia Giulia': [ + 'Collio', + 'Colli Orientali del Friuli', + 'Friuli Grave del Friuli' + ], + 'Trentino-Alto Adige': ['Trentino', 'Alto Adige'], + Lombardy: [], + Franciacorta: [], + Valtellina: [], + 'Emilia-Romagna': ['Lambrusco'], + Umbria: ['Orvieto', 'Torgiano'], + Lazio: ['Frascati'], + Abruzzo: [], + Molise: ['Biferno', 'Pentro'], + Campania: ['Taurasi', 'Aglianico del Vulture', 'Fiano di Avellino'], + Puglia: ['Primitivo di Manduria', 'Salice Salentino'], + Basilicata: [], + Calabria: ['Cirò', 'Greco di Bianco'], + Sicily: ['Etna', 'Marsala', 'Pantelleria'], + Sardinia: ['Vermentino di Gallura', 'Cannonau di Sardegna'] + }, + FR: { + Alsace: [], + Ardeche: [], + Aquitaine: [], + Bordeaux: { + Médoc: ['Pauillac', 'Margaux', 'Saint-Estèphe', 'Saint-Julien'], + 'Saint-Émilion': [], + Pomerol: [], + Graves: ['Pessac-Léognan'], + Sauternes: ['Barsac'] + }, + Burgundy: { + Chablis: [], + 'Côte de Nuits': [ + 'Gevrey-Chambertin', + 'Vosne-Romanée', + 'Nuits-Saint-Georges' + ], + 'Côte de Beaune': ['Meursault', 'Puligny-Montrachet', 'Beaune'], + 'Côte Chalonnaise': ['Mercurey'], + Mâconnais: ['Pouilly-Fuissé'], + Beaujolais: ['Morgon', 'Fleurie', 'Moulin a Vent'] + }, + Champagne: ['Montagne de Reims', 'Vallée de la Marne', 'Côte des Blancs'], + 'Loire Valley': [ + 'Sancerre', + 'Pouilly-Fumé', + 'Vouvray', + 'Muscadet', + 'Anjou', + 'Saumur', + 'Chinon' + ], + 'Rhône Valley': [ + 'Côte-Rôtie', + 'Hermitage', + 'Condrieu', + 'Saint-Joseph', + 'Châteauneuf-du-Pape', + 'Côtes du Rhône', + 'Gigondas', + 'Vacqueyras' + ], + Provence: ['Côtes de Provence', 'Bandol'], + 'Languedoc-Roussillon': [], + 'Southwest France': ['Cahors', 'Madiran', 'Jurançon'], + Jura: ['Arbois', 'Côtes du Jura'], + Savoie: [], + Corsica: ['Patrimonio'] + }, + DE: { + Mosel: ['Saar', 'Ruwer'], + Pfalz: [], + Rheinhessen: [], + Nahe: [], + Rheingau: [], + 'Hessische Bergstraße': [], + Franken: [], + Baden: [], + Württemberg: [], + 'Saale-Unstrut': [], + Sachsen: [], + Ahr: [] + }, + CH: { + Vaud: ['Lavaux', 'Vevey', 'Morges', 'Nyon'], + Valais: ['Sierre', 'Sion', 'Martigny'], + Geneva: ['Lancy', 'Carouge'], + Ticino: ['Locarno', 'Bellinzona', 'Ascona'], + Neuchâtel: ['Val-de-Travers', 'Val-de-Ruz'], + Fribourg: ['Glâne', 'Sarine'], + Bern: ['Thun', 'Biel'], + Aargau: ['Baden', 'Brugg'], + Thurgau: ['Frauenfeld', 'Arbon'], + Zürich: ['Affoltern', 'Dietikon'] + }, + ES: { + Rioja: ['Rioja Alta', 'Rioja Alavesa', 'Rioja Oriental'], + Navarra: [], + 'Rías Baixas': [], + 'Ribeira Sacra': [], + Valdeorras: [], + Txakoli: ['Getariako Txakolina', 'Bizkaiko Txakolina', 'Arabako Txakolina'], + 'Ribera del Duero': [], + Rueda: [], + Toro: [], + Cigales: [], + 'La Mancha': [], + Valdepeñas: [], + Priorat: [], + Penedès: [], + Cava: [], + Montsant: [], + 'Conca de Barberà': [], + 'Jerez-Xérès': [], + 'Montilla-Moriles': [], + Málaga: [], + 'Sierras de Málaga': [], + 'Condado de Huelva': [], + 'Utiel-Requena': [], + Valencia: [], + Jumilla: [], + Yecla: [], + Bullas: [], + 'Canary Islands': [], + 'Balearic Islands': ['Binissalem', 'Pla i Llevant'], + Bierzo: [], + 'Campo de Borja': [], + Aragón: ['Calatayud', 'Somontano'], + 'Ribera del Guadiana': [] + }, + PT: { + Douro: ['Baixo Corgo', 'Cima Corgo', 'Douro Superior'], + Alentejo: ['Central', 'Litoral', 'Interior Norte', 'Interior Sul'], + Porto: ['Gaia'], + 'Vinho Verde': [ + 'Amarante', + 'Ave', + 'Baião', + 'Basto', + 'Cávado', + 'Lima', + 'Lima Interior', + 'Lima Litoral', + 'Monção', + 'Paiva', + 'Sousa' + ], + Tejo: [ + 'Alcobaça', + 'Arruda dos Vinhos', + 'Azeiteira', + 'Benavente e Santarém', + 'Bico do Cachorro', + 'Borba', + 'Casaínhos', + 'Chamusca', + 'Colares', + 'Coruche', + 'Fátima', + 'Figueira da Foz', + 'Golegã', + 'Gualtar', + 'Gândara', + 'Mação', + 'Mafra', + 'Montemor-o-Novo', + 'Montijo', + 'Odivelas', + 'Palmela', + 'Pataias', + 'Peniche', + 'Porto de Mós', + 'Reguengos de Monsaraz', + 'Ribatejo', + 'Sabugal', + 'Setúbal', + 'Tomar', + 'Torres Vedras', + 'Vila Franca de Xira' + ], + Lisbon: [ + 'Alenquer', + 'Arruda', + 'Carcavelos e Cascais', + 'Colares', + 'Encoro', + 'Estremoz', + 'Oeiras', + 'Palmela', + 'Setúbal' + ], + Madeira: [ + 'Machico', + 'Santa Cruz', + 'Caniço', + 'Santo António da Serra', + 'Ponta do Sol', + 'Calheta', + 'Funchal', + 'Câmara de Lobos', + 'Ribeira Brava', + 'Paul do Mar', + 'Canhas', + 'Arco da Calheta', + 'São Vicente', + 'Porto Moniz', + 'Paul da Serra' + ] + }, + AT: { + Wagram: ['Marchfeld'], + Kremstal: ['Krems', 'Spitz'], + Kamptal: ['Langenlois', 'Gumpoldskirchen'], + Wachau: [], + Thermenregion: ['Baden', 'Mödling'], + Wienerwald: [], + Südsteiermark: ['Grazer Bergland', 'Leibnitz', 'Deutschlandsberg'], + Burgenland: ['Neusiedlersee', 'Eisenberg', 'Rosalia'], + Styria: ['Südsteiermark', 'Weststeiermark'], + Salzburg: ['Flachgau'], + Tyrol: ['Innsbruck', 'Hall'], + Carinthia: ['Villach', 'Klagenfurt'] + }, + HU: { + Tokaj: [], + Eger: [], + Badacsony: [], + Balaton: [ + 'Balatonalmádi', + 'Balatonboglár', + 'Balatonfüred-Csopak', + 'Balatonlelle', + 'Balatonfüred', + 'Somló' + ], + Mátra: [], + Hegyalja: [], + Pannon: ['Szekszárd', 'Pécs', 'Baja', 'Sopron'], + Villány: [] + }, + CZ: { + 'South Moravia': ['Znojmo', 'Břeclav', 'Hodonín', 'Kyjov', 'Vranov'], + 'Zlín Region': ['Valašské Meziříčí', 'Kroměříž', 'Vsetín'], + 'Olomouc Region': ['Prostějov', 'Olomouc', 'Litomyšl'], + 'South Bohemia': ['České Budějovice', 'Písek', 'Strakonice'], + Plzeň: ['Rokycany', 'Domažlice'] + }, + MD: ['Iași', 'Botoșani', 'Vaslui', 'Neamț'], + RO: { + Transylvania: ['Cluj', 'Sibiu', 'Brașov', 'Mureș'], + Muntenia: ['Buzău', 'Dâmbovița', 'Prahova', 'Vrancea'], + Oltenia: ['Dolj', 'Gorj', 'Vâlcea', 'Mehedinți'], + Banat: ['Arad', 'Timiș', 'Caraș-Severin'], + Dobrogea: ['Constanța', 'Tulcea'], + Crisana: ['Bihor'], + Maramureș: ['Suceava', 'Maramureș'] + }, + GR: { + Attica: [], + 'Central Greece': [ + 'Euboea', + 'Boeotia', + 'Phocis', + 'Locris', + 'Phthiotis', + 'Phocis', + 'Locris', + 'Phthiotis' + ], + Peloponnese: [ + 'Nemea', + 'Mantineia', + 'Achaia', + 'Messenia', + 'Laconia', + 'Arcadia', + 'Argolis', + 'Corinthia' + ], + Thrace: [], + Macedonia: [ + 'Thessaloniki', + 'Kilkis', + 'Pella', + 'Florina', + 'Kastoria', + 'Imathia', + 'Pella', + 'Florina', + 'Kastoria' + ], + Thessaly: ['Trikala', 'Larissa', 'Magnesia', 'Karditsa', 'Trikala'], + 'Ionian Islands': [ + 'Corfu', + 'Zakynthos', + 'Kefalonia', + 'Lefkada', + 'Ithaca', + 'Paxos', + 'Corfu', + 'Zakynthos', + 'Kefalonia', + 'Lefkada', + 'Ithaca', + 'Paxos' + ], + Cyclades: [ + 'Santorini', + 'Paros', + 'Naxos', + 'Mykonos', + 'Sifnos', + 'Tinos', + 'Santorini', + 'Paros', + 'Naxos', + 'Mykonos', + 'Sifnos', + 'Tinos' + ], + Crete: [], + 'Aegean Islands': [ + 'Samos', + 'Lesbos', + 'Chios', + 'Limnos', + 'Ikaria', + 'Samos', + 'Lesbos', + 'Chios', + 'Limnos', + 'Ikaria' + ] + }, + HR: { + Dalmatia: ['Hvar', 'Brac', 'Korcula', 'Peljesac', 'Dubrovnik-Riviera'], + Istria: [], + Slavonia: [], + Kvarner: ['Kvarner Bay', 'Labin'], + 'Continental Croatia': ['Srijem', 'Moslavina', 'Podravina'], + 'Central Croatia': ['Zagorje', 'Medgurje', 'Bilogora'] + }, + GE: { + Kakheti: ['Telavi', 'Sighnaghi', 'Akhasheni', 'Kvareli'], + Imereti: ['Kutaisi', 'Khoni', 'Tsqaltubo', 'Sachkhere'], + 'Racha-Lechkhumi': [], + 'Kvemo Svaneti': [], + Kartli: ['Tbilisi', 'Gori', 'Kaspi', 'Rustavi'], + Adjara: ['Batumi', 'Kobuleti'], + 'Samegrelo-Zemo Svaneti': ['Poti', 'Zugdidi'], + Abkhazia: ['Gagra', 'Gudauta', 'Ochamchire'], + 'South Georgia': ['Adigeni', 'Akhalkalaki', 'Bolnisi', 'Ninotsminda'] + }, + IL: { + Galilee: ['Golan Heights', 'Carmel Mountains'], + 'Central District': ['Shomron', 'Shfela'], + Negev: ['Southern Negev', 'Central Negev'], + 'Coastal Plain': ['Central Coastal Plain', 'Southern Coastal Plain'] + }, + LB: ['Bekaa Valley', 'Mount Lebanon', 'North Lebanon', 'South Lebanon'], + TR: { + Anatolia: [ + 'Konya', + 'Aksaray', + 'Nevşehir', + 'Erzurum', + 'Van', + 'Bitlis', + 'İzmir', + 'Manisa', + 'Ayvalık' + ], + 'Aegean Region': ['Urla', 'Foça', 'Bornova'], + Ayvalık: [], + 'Marmara Region': ['İstanbul', 'Edirne'], + 'Black Sea Region': ['Rize', 'Artvin', 'Trabzon'], + 'Southeastern Anatolia': ['Gaziantep', 'Şanlıurfa', 'Adana'] + }, + AM: ['Vayots Dzor', 'Ararat Valley', 'Gegharkunik', 'Tavush', 'Syunik'], + CA: { + Ontario: [ + 'Niagara Peninsula', + 'Lake Erie North Shore', + 'Pelee Island', + 'Prince Edward County' + ], + 'British Columbia': [ + 'Okanagan Valley', + 'Similkameen Valley', + 'Fraser Valley', + 'Southern Gulf Islands' + ], + Quebec: ['Montreal', 'Eastern Townships', 'Charlevoix'], + 'Nova Scotia': ['Annapolis Valley', 'Gaspereau Valley', 'North Shore'], + 'New Brunswick': ['Annapolis Valley', 'Gaspereau Valley'], + 'Prince Edward Island': ['Eastern PEI', 'Western PEI'] + }, + US: { + California: [ + 'Napa Valley', + 'Sonoma Valley', + 'Paso Robles', + 'Santa Barbara County', + 'Central Coast', + 'Lodi', + 'Mendocino County', + 'Santa Cruz Mountains', + 'Russian River Valley' + ], + Oregon: [ + 'Willamette Valley', + 'Rogue Valley', + 'Umpqua Valley', + 'Southern Oregon', + 'Oregon Coast' + ], + Washington: [ + 'Columbia Valley', + 'Walla Walla Valley', + 'Red Mountain', + 'Snake River' + ], + 'New York': [ + 'Finger Lakes', + 'Long Island', + 'Hudson River', + 'Western New York' + ], + Virginia: [ + 'Monticello', + 'Shenandoah Valley', + 'Northern Virginia', + 'Virginia Piedmont' + ], + 'Texas Hill Country': [], + 'Arizona Sonoita': [], + 'Colorado Grand Valley': [], + 'Idaho Snake River Valley': [], + Michigan: ['Michigan Lake', 'Michigan Shore'] + }, + AR: { + Mendoza: ['Luján de Cuyo', 'Uco Valley', 'Maipú', 'San Rafael'], + Salta: ['Calchaquí Valleys', 'Quebrada de Humahuaca'], + 'San Juan': ['Ullum', 'Calingasta', 'Tulum'], + 'La Rioja': [], + Catamarca: ['Andalgalá', 'Belén'], + 'San Luis': [ + 'Valle del Conlara', + 'Valle del Potrerillos', + 'Valle de las Carreras' + ], + 'Buenos Aires': [ + 'Partido de General Belgrano', + 'Partido de Lobos', + 'Partido de Tandil' + ], + Neuquén: ['Valle de Río Negro', 'Valle de Agrelo'] + }, + CL: { + 'Central Valley': [ + 'Maipo Valley', + 'Casablanca Valley', + 'San Antonio Valley', + 'Maule Valley', + 'Colchagua Valley' + ], + 'Coastal Regions': [ + 'Valle de Aconcagua', + 'Valle de Limarí', + 'Valle de Elqui' + ], + 'Southern Chile': ['Valle de Itata', 'Valle de Bío-Bío'], + 'Northern Chile': ['Valle de Elqui'] + }, + ZA: { + 'Western Cape': { + Stellenbosch: [ + 'Simonsberg-Stellenbosch', + 'Jonkershoek-Stellenbosch', + 'Stellenbosch Mountain' + ], + Paarl: ['Franschhoek', 'Paarl Mountain'], + Constantia: ['Constantia Valley'], + Durbanville: ['Durbanville Hills'], + 'Walker Bay': ['Hemel-en-Aarde'], + Swartland: ['Paardeberg'] + }, + 'Eastern Cape': ['Jeffreys Bay'], + 'Northern Cape': ['Augrabies'] + }, + AU: { + 'South Eastern Australia': [], + 'New South Wales': [ + 'Hunter Valley', + 'Riverina', + 'Mudgee', + 'Orange', + 'Cowra', + 'Hilltops', + 'Tumbarumba', + 'Gundagai', + 'Shoalhaven Coast', + 'Southern Highlands', + 'New England' + ], + 'South Australia': [ + 'Barossa Valley', + 'McLaren Vale', + 'Clare Valley', + 'Coonawarra', + 'Adelaide Hills', + 'Eden Valley', + 'Langhorne Creek', + 'Padthaway', + 'Kangaroo Island', + 'Mount Gambier', + 'Robe', + 'Wrattonbully', + 'Fleurieu Peninsula', + 'Currency Creek', + 'Southern Fleurieu' + ], + Victoria: [ + 'Yarra Valley', + 'Mornington Peninsula', + 'Geelong', + 'Bellarine Peninsula', + 'Sunbury', + 'Macedon Ranges', + 'Heathcote', + 'Bendigo', + 'Pyrenees', + 'Grampians', + 'Great Western', + 'Henty', + 'Otway Ranges', + 'King Valley', + 'Alpine Valleys', + 'Rutherglen', + 'Glenrowan', + 'Goulburn Valley', + 'Strathbogie Ranges' + ], + 'Western Australia': [ + 'Margaret River', + 'Great Southern', + 'Swan Valley', + 'Pemberton', + 'Manjimup', + 'Blackwood Valley', + 'Geographe', + 'Peel', + 'Perth Hills', + 'Frankland River', + 'Mount Barker', + 'Porongurup', + 'Denmark', + 'Albany', + 'Esperance' + ], + Tasmania: [ + 'Tamar Valley', + 'Pipers River', + 'East Coast', + 'Coal Valley', + 'Derwent Valley', + 'Huon Valley' + ], + Queensland: [ + 'Granite Belt', + 'South Burnett', + 'Darling Downs', + 'Scenic Rim' + ], + 'Canberra District': [ + 'Canberra', + 'Murrumbateman', + 'Yass Valley', + 'Gundaroo', + 'Lake George' + ] + }, + NZ: { + 'North Island': [ + 'Auckland', + 'Gisborne', + `Hawke's Bay`, + 'Wairarapa', + 'Bay of Islands', + 'Coromandel', + 'Waikato' + ], + 'South Island': [ + 'Marlborough', + 'Nelson', + 'Waipara', + 'Canterbury', + 'Central Otago', + 'Waipara Valley', + 'North Canterbury', + 'North Otago' + ] + }, + CN: { + Ningxia: { + Yinchuan: [], + 'Helan Mountain': ['Qingtongxia', 'Wucaiwan', 'Qingshuihe', 'Jinfeng'] + }, + Shaanxi: ['Lishan', 'Fengxiang'], + Shandong: ['Yantai', 'Penglai', 'Weihai'], + Xinjiang: ['Turpan', 'Yili', 'Jinghe'], + Hebei: ['Changli', 'Qinhuangdao'], + Heilongjiang: ['Yichun', 'Qiqihar'], + Jilin: ['Baishan', 'Changchun'], + Liaoning: ['Dalian', 'Shenyang'], + 'Inner Mongolia': ['Hohhot', 'Baotou'], + Guangxi: ['Nanning', 'Liuzhou'], + Hunan: ['Changsha', 'Yongzhou'], + Hubei: ['Wuhan'], + Sichuan: ['Chengdu', `Ya'an`], + Guizhou: ['Guiyang', 'Zunyi'], + Yunnan: ['Kunming', 'Dali'] + }, + JP: { + 'Yamanashi Prefecture': ['Kofu Basin', 'Katsunuma', 'Fuefuki'], + 'Hokkaido Prefecture': ['Biei', 'Tomakomai', 'Nanae'], + 'Niigata Prefecture': ['Uonuma', 'Sannosawa'], + 'Nagano Prefecture': ['Kiso Valley', 'Nagano'], + 'Gifu Prefecture': ['Gujo', 'Mino'], + 'Aichi Prefecture': ['Nishio', 'Seto'], + 'Shizuoka Prefecture': ['Suruga', 'Shizuoka'], + 'Kanagawa Prefecture': ['Yokohama', 'Kawasaki'], + 'Chiba Prefecture': ['Noda', 'Chiba'], + 'Ibaraki Prefecture': ['Tsukuba', 'Hitachi'], + 'Tochigi Prefecture': ['Utsunomiya', 'Tochigi'], + 'Gunma Prefecture': ['Maebashi', 'Gunma'] + }, + UK: { + Kent: ['Ashford', 'Canterbury', 'Dover', 'Maidstone', 'Sevenoaks'], + Sussex: ['Brighton', 'Eastbourne', 'Hastings', 'Chichester', 'Horsham'], + Hampshire: ['Winchester', 'Petersfield', 'Basingstoke', 'Andover'], + Wiltshire: ['Swindon', 'Chippenham', 'Salisbury', 'Trowbridge'], + Dorset: ['Poole', 'Bournemouth', 'Dorset'], + Devon: ['Exeter', 'Newton Abbot', 'Plymouth', 'Torquay'], + Somerset: ['Bath', 'Weston-super-Mare', 'Taunton', 'Yeovil'], + Gloucestershire: ['Cheltenham', 'Gloucester', 'Cirencester', 'Stroud'], + Worcestershire: ['Worcester', 'Malvern', 'Kidderminster', 'Bewdley'], + Warwickshire: [ + 'Warwick', + 'Stratford-upon-Avon', + 'Leamington Spa', + 'Kenilworth' + ], + Herefordshire: ['Hereford', 'Leominster', 'Ross-on-Wye', 'Kington'], + Shropshire: ['Shrewsbury', 'Telford', 'Ludlow', 'Oswestry'] + }, + UY: { + 'Canelones Department': ['San Carlos', 'Melo', 'Paso de los Toros'], + 'Montevideo Department': ['Carrasco', 'Punta del Este'], + 'Colonia Department': ['Colonia del Sacramento', 'Villa Soriano'], + 'San José Department': ['Progreso', 'Treinta y Tres'], + 'Rocha Department': ['La Paloma', 'Rocha Sur'], + 'Florida Department': ['Florida', 'Sarandí del Yí'], + 'Soriano Department': ['Soriano', 'Río Negro'], + 'Tacuarembó Department': ['Tacuarembó', 'Paso de los Toros'], + 'Treinta y Tres Department': ['Treinta y Tres', 'Rivera'] + } +}