Tools: Building the Yamaokaya Map (Unofficial)

Tools: Building the Yamaokaya Map (Unofficial)

Source: Dev.to

About Yamaokaya Map (Unofficial) ## Ramen Yamaokaya Store Types ## Advance Preparation ## Contacting the Official Website ## Data Acquisition and Processing ## Map Application ## Publishing Settings in Amplify Gen2 ## Data Acquisition and Processing ## Scraping ## DMS→DD Conversion ## Column Name Change ## CSV to GeoJSON Conversion ## Creating the Map Application ## Setting the Background Map ## Layer Configuration ## Adding GeoJSON Layers ## Implementing Popups ## Layer Toggling ## Summary Do you know about the wonderful ramen chain “Ramen Yamaokaya” in Japan? Ramen Yamaokaya is a nationwide ramen chain founded in 1988 in Ushiku City, Ibaraki Prefecture, Japan. It's known for its rich tonkotsu broth and for allowing you to freely customize noodle firmness, flavor intensity, and fat content. Many locations are open 24 hours, making it beloved by truck drivers and night shift workers. I myself have been a fan for over 20 years. My home store is the legendary “Minami 2-jo Store” in Sapporo. I always order the shoyu ramen with less fat. Last year's AWS Summit Japan 2025 inspired me to think about how I could support Yamaokaya within my area of expertise. So, I built an unofficial web application called “Yamaokaya Map.” This map lets you view store information for Ramen Yamaokaya locations nationwide. https://yama.dayjournal.dev This app supports PWA, so you can add it to your smartphone's home screen. Ramen Yamaokaya has four store types. 1. Ramen Yamaokaya The standard Yamaokaya. Offers classic tonkotsu-based menu items. There are over 150 locations nationwide. I always order the Shoyu Ramen. 2. Niboshi Ramen Yamaokaya A specialty shop serving niboshi (dried sardine) broth ramen. You can enjoy a different flavor profile from the standard Yamaokaya. I'm not a fan of niboshi, so I've actually never been. 3. Miso Ramen Yamaokaya A shop specializing in miso ramen, known for its rich miso soup. Here, I recommend ordering the Shoyu Ramen deliberately. There are only 3 locations, all in Hokkaido. 4. Gyoza no Yamaokaya A new concept store focusing on gyoza. There's only one location in all of Japan, located in Sapporo. The map released this time uses icons to distinguish these four store types, and you can toggle their display on or off via layer switching. Since I was going to perform scraping this time, I checked with the official website beforehand. They gave me a very warm response. Thanks to that, I immediately wanted to go eat there again. This time, I'll use Python for scraping. I'll combine Playwright, pandas, and geopy to acquire and process the data. First, fork the Amazon Location Service v2 starter template. Then, add the files and code needed for the Yamaokaya Map. MapLibre GL JS & Amazon Location Service Starter Execution environment Using the starter repository I forked, I’ll publish it on GitHub in the Amplify Console (Gen2), referencing an article I wrote previously. https://memo.dayjournal.dev/memo/aws-amplify-016 The script scrapes store information from the official website. Since the official site dynamically generates content, I use Playwright to control the browser and retrieve the data. From each store's detail page, I extract the store name, address, phone number, business hours, parking information, seat types, shower room availability, the detail page URL, and the store's location information. Example of retrieving the store name The location data scraped is in DMS (degrees, minutes, seconds) format. To display it with the map library, I convert it to DD format (decimal degrees). I use geopy to handle multiple conversion patterns. Example of DMS→DD conversion Before converting the data to GeoJSON, I change Japanese column names to English. Example of column name change Finally, I convert the CSV to GeoJSON format. Files are output separately for each store type. Example of CSV to GeoJSON conversion GeoJSON output result For this project, I use MapLibre GL JS as the map library and Amazon Location Service for the background map. I set up layers for each store type and assign custom icons to them. I add the GeoJSON data as layers. I configure the icon size to change based on the zoom level. Clicking a store icon displays the store information in a popup. It shows the address, phone number, business hours, parking information, seating types, etc. Implemented layer toggling (show/hide) using maplibre-gl-opacity. This time, I built the "Yamaokaya Map (Unofficial)" using a structure that includes Playwright for scraping, geopy for DMS→DD conversion and CSV→GeoJSON conversion, and map display via MapLibre GL JS and Amazon Location Service. Visualizing this on a map reveals new insights. The northernmost store is in Wakkanai. Stores are located in surrounding areas rather than central Tokyo. While they have expanded into Kyushu, there are no stores in Shikoku. And there is only one Gyoza no Yamaokaya store nationwide. This way, Ramen Yamaokaya's store opening strategy becomes clear. Please use this when searching for a nearby store or looking for Ramen Yamaokaya while traveling! Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK: yamaokaya-data └── script ├── scrape_yamaokaya.py ├── latlon_yamaokaya.py ├── column_yamaokaya.py ├── csv2geojson.py Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: yamaokaya-data └── script ├── scrape_yamaokaya.py ├── latlon_yamaokaya.py ├── column_yamaokaya.py ├── csv2geojson.py CODE_BLOCK: yamaokaya-data └── script ├── scrape_yamaokaya.py ├── latlon_yamaokaya.py ├── column_yamaokaya.py ├── csv2geojson.py CODE_BLOCK: yamaokaya-map ├── LICENSE ├── README.md ├── dist │ └── index.html ├── img │ ├── README01.gif │ ├── README02.png │ └── README03.png ├── index.html ├── package-lock.json ├── package.json ├── public │ ├── manifest.json │ ├── data │ │ ├── yama.geojson │ │ ├── niboshi.geojson │ │ ├── miso.geojson │ │ └── gyouza.geojson │ └── icons │ ├── yama.png │ ├── niboshi.png │ ├── miso.png │ └── gyouza.png ├── src │ ├── main.ts │ ├── style.css │ └── vite-env.d.ts ├── tsconfig.json └── vite.config.ts Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: yamaokaya-map ├── LICENSE ├── README.md ├── dist │ └── index.html ├── img │ ├── README01.gif │ ├── README02.png │ └── README03.png ├── index.html ├── package-lock.json ├── package.json ├── public │ ├── manifest.json │ ├── data │ │ ├── yama.geojson │ │ ├── niboshi.geojson │ │ ├── miso.geojson │ │ └── gyouza.geojson │ └── icons │ ├── yama.png │ ├── niboshi.png │ ├── miso.png │ └── gyouza.png ├── src │ ├── main.ts │ ├── style.css │ └── vite-env.d.ts ├── tsconfig.json └── vite.config.ts CODE_BLOCK: yamaokaya-map ├── LICENSE ├── README.md ├── dist │ └── index.html ├── img │ ├── README01.gif │ ├── README02.png │ └── README03.png ├── index.html ├── package-lock.json ├── package.json ├── public │ ├── manifest.json │ ├── data │ │ ├── yama.geojson │ │ ├── niboshi.geojson │ │ ├── miso.geojson │ │ └── gyouza.geojson │ └── icons │ ├── yama.png │ ├── niboshi.png │ ├── miso.png │ └── gyouza.png ├── src │ ├── main.ts │ ├── style.css │ └── vite-env.d.ts ├── tsconfig.json └── vite.config.ts COMMAND_BLOCK: npm install Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: npm install COMMAND_BLOCK: npm install COMMAND_BLOCK: from playwright.sync_api import sync_playwright import pandas as pd def scrape_yamaokaya_shops(): shops = [] with sync_playwright() as p: browser = p.chromium.launch(headless=True) context = browser.new_context( user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', ) context.set_default_timeout(10000) context.set_default_navigation_timeout(10000) page = context.new_page() main_url = "https://www.yamaokaya.com/shops/" page.goto(main_url, wait_until='networkidle', timeout=10000) page.wait_for_timeout(5000) shop_links = page.eval_on_selector_all( 'a[href*="/shops/"]', 'els => [...new Set(els.map(el => el.href).filter(href => /shops\\/\\d+/.test(href)))]' ) for url in shop_links: try: page.goto(url, wait_until='domcontentloaded', timeout=10000) page.wait_for_timeout(5000) name = page.evaluate("""() => { const h = document.querySelector('h2, h1, .shop-name'); return h?.innerText?.trim() || document.title.split('|')[0].trim(); }""") shops.append({'url': url, 'name': name or '不明'}) except Exception as e: shops.append({'url': url, 'name': 'エラー'}) browser.close() return pd.DataFrame(shops) if __name__ == "__main__": df = scrape_yamaokaya_shops() df.to_csv('yamaokaya_shops.csv', index=False, encoding='utf-8-sig') Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: from playwright.sync_api import sync_playwright import pandas as pd def scrape_yamaokaya_shops(): shops = [] with sync_playwright() as p: browser = p.chromium.launch(headless=True) context = browser.new_context( user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', ) context.set_default_timeout(10000) context.set_default_navigation_timeout(10000) page = context.new_page() main_url = "https://www.yamaokaya.com/shops/" page.goto(main_url, wait_until='networkidle', timeout=10000) page.wait_for_timeout(5000) shop_links = page.eval_on_selector_all( 'a[href*="/shops/"]', 'els => [...new Set(els.map(el => el.href).filter(href => /shops\\/\\d+/.test(href)))]' ) for url in shop_links: try: page.goto(url, wait_until='domcontentloaded', timeout=10000) page.wait_for_timeout(5000) name = page.evaluate("""() => { const h = document.querySelector('h2, h1, .shop-name'); return h?.innerText?.trim() || document.title.split('|')[0].trim(); }""") shops.append({'url': url, 'name': name or '不明'}) except Exception as e: shops.append({'url': url, 'name': 'エラー'}) browser.close() return pd.DataFrame(shops) if __name__ == "__main__": df = scrape_yamaokaya_shops() df.to_csv('yamaokaya_shops.csv', index=False, encoding='utf-8-sig') COMMAND_BLOCK: from playwright.sync_api import sync_playwright import pandas as pd def scrape_yamaokaya_shops(): shops = [] with sync_playwright() as p: browser = p.chromium.launch(headless=True) context = browser.new_context( user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', ) context.set_default_timeout(10000) context.set_default_navigation_timeout(10000) page = context.new_page() main_url = "https://www.yamaokaya.com/shops/" page.goto(main_url, wait_until='networkidle', timeout=10000) page.wait_for_timeout(5000) shop_links = page.eval_on_selector_all( 'a[href*="/shops/"]', 'els => [...new Set(els.map(el => el.href).filter(href => /shops\\/\\d+/.test(href)))]' ) for url in shop_links: try: page.goto(url, wait_until='domcontentloaded', timeout=10000) page.wait_for_timeout(5000) name = page.evaluate("""() => { const h = document.querySelector('h2, h1, .shop-name'); return h?.innerText?.trim() || document.title.split('|')[0].trim(); }""") shops.append({'url': url, 'name': name or '不明'}) except Exception as e: shops.append({'url': url, 'name': 'エラー'}) browser.close() return pd.DataFrame(shops) if __name__ == "__main__": df = scrape_yamaokaya_shops() df.to_csv('yamaokaya_shops.csv', index=False, encoding='utf-8-sig') COMMAND_BLOCK: from typing import Tuple from geopy import Point # 変換前 "43°03'28.6""N 141°21'22.2""E" def _convert_with_geopy(dms_string: str) -> Tuple[float, float]: cleaned = dms_string.replace('""', '"') point = Point(cleaned) return point.latitude, point.longitude Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: from typing import Tuple from geopy import Point # 変換前 "43°03'28.6""N 141°21'22.2""E" def _convert_with_geopy(dms_string: str) -> Tuple[float, float]: cleaned = dms_string.replace('""', '"') point = Point(cleaned) return point.latitude, point.longitude COMMAND_BLOCK: from typing import Tuple from geopy import Point # 変換前 "43°03'28.6""N 141°21'22.2""E" def _convert_with_geopy(dms_string: str) -> Tuple[float, float]: cleaned = dms_string.replace('""', '"') point = Point(cleaned) return point.latitude, point.longitude CODE_BLOCK: column_mapping = { '店舗名': 'store_name', '住所': 'address', '電話番号': 'phone_number', '営業時間': 'business_hours', '駐車場': 'parking', '座席の種類': 'seating_types', 'シャワー室': 'shower_room', 'その他': 'other_info' } df_renamed = df.rename(columns=column_mapping) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: column_mapping = { '店舗名': 'store_name', '住所': 'address', '電話番号': 'phone_number', '営業時間': 'business_hours', '駐車場': 'parking', '座席の種類': 'seating_types', 'シャワー室': 'shower_room', 'その他': 'other_info' } df_renamed = df.rename(columns=column_mapping) CODE_BLOCK: column_mapping = { '店舗名': 'store_name', '住所': 'address', '電話番号': 'phone_number', '営業時間': 'business_hours', '駐車場': 'parking', '座席の種類': 'seating_types', 'シャワー室': 'shower_room', 'その他': 'other_info' } df_renamed = df.rename(columns=column_mapping) CODE_BLOCK: import json import pandas as pd def create_geojson_features(df): features = [] for _, row in df.iterrows(): properties = {} for col in df.columns: if col not in ['lat', 'lon']: value = row[col] if pd.isna(value): properties[col] = None else: properties[col] = str(value) feature = { "type": "Feature", "geometry": { "type": "Point", "coordinates": [row['lon'], row['lat']] }, "properties": properties } features.append(feature) return features Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: import json import pandas as pd def create_geojson_features(df): features = [] for _, row in df.iterrows(): properties = {} for col in df.columns: if col not in ['lat', 'lon']: value = row[col] if pd.isna(value): properties[col] = None else: properties[col] = str(value) feature = { "type": "Feature", "geometry": { "type": "Point", "coordinates": [row['lon'], row['lat']] }, "properties": properties } features.append(feature) return features CODE_BLOCK: import json import pandas as pd def create_geojson_features(df): features = [] for _, row in df.iterrows(): properties = {} for col in df.columns: if col not in ['lat', 'lon']: value = row[col] if pd.isna(value): properties[col] = None else: properties[col] = str(value) feature = { "type": "Feature", "geometry": { "type": "Point", "coordinates": [row['lon'], row['lat']] }, "properties": properties } features.append(feature) return features CODE_BLOCK: { "type": "Feature", "geometry": { "type": "Point", "coordinates": [ 141.3561, 43.0579 ]}, "properties": { "store_name": "ラーメン山岡家 南2条店", "details": "https://www.yamaokaya.com/shops/1102/", "address": "札幌市中央区南2条西1丁目6-1", "phone_number": "(011) 242-4636", "business_hours": "5:00-翌4:00", "parking": "なし", "seating_types": "カウンター席: 13", "shower_room": "なし", "other_info": "まちなかのちいさなお店です。" } }, Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: { "type": "Feature", "geometry": { "type": "Point", "coordinates": [ 141.3561, 43.0579 ]}, "properties": { "store_name": "ラーメン山岡家 南2条店", "details": "https://www.yamaokaya.com/shops/1102/", "address": "札幌市中央区南2条西1丁目6-1", "phone_number": "(011) 242-4636", "business_hours": "5:00-翌4:00", "parking": "なし", "seating_types": "カウンター席: 13", "shower_room": "なし", "other_info": "まちなかのちいさなお店です。" } }, CODE_BLOCK: { "type": "Feature", "geometry": { "type": "Point", "coordinates": [ 141.3561, 43.0579 ]}, "properties": { "store_name": "ラーメン山岡家 南2条店", "details": "https://www.yamaokaya.com/shops/1102/", "address": "札幌市中央区南2条西1丁目6-1", "phone_number": "(011) 242-4636", "business_hours": "5:00-翌4:00", "parking": "なし", "seating_types": "カウンター席: 13", "shower_room": "なし", "other_info": "まちなかのちいさなお店です。" } }, CODE_BLOCK: import './style.css'; import 'maplibre-gl/dist/maplibre-gl.css'; import 'maplibre-gl-opacity/dist/maplibre-gl-opacity.css'; import maplibregl from 'maplibre-gl'; import OpacityControl from 'maplibre-gl-opacity'; const region = import.meta.env.VITE_REGION; const mapApiKey = import.meta.env.VITE_MAP_API_KEY; const mapName = import.meta.env.VITE_MAP_NAME; const map = new maplibregl.Map({ container: 'map', style: `https://maps.geo.${region}.amazonaws.com/maps/v0/maps/${mapName}/style-descriptor?key=${mapApiKey}`, center: [138.0000, 38.5000], zoom: baseZoom, maxZoom: 20 }); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: import './style.css'; import 'maplibre-gl/dist/maplibre-gl.css'; import 'maplibre-gl-opacity/dist/maplibre-gl-opacity.css'; import maplibregl from 'maplibre-gl'; import OpacityControl from 'maplibre-gl-opacity'; const region = import.meta.env.VITE_REGION; const mapApiKey = import.meta.env.VITE_MAP_API_KEY; const mapName = import.meta.env.VITE_MAP_NAME; const map = new maplibregl.Map({ container: 'map', style: `https://maps.geo.${region}.amazonaws.com/maps/v0/maps/${mapName}/style-descriptor?key=${mapApiKey}`, center: [138.0000, 38.5000], zoom: baseZoom, maxZoom: 20 }); CODE_BLOCK: import './style.css'; import 'maplibre-gl/dist/maplibre-gl.css'; import 'maplibre-gl-opacity/dist/maplibre-gl-opacity.css'; import maplibregl from 'maplibre-gl'; import OpacityControl from 'maplibre-gl-opacity'; const region = import.meta.env.VITE_REGION; const mapApiKey = import.meta.env.VITE_MAP_API_KEY; const mapName = import.meta.env.VITE_MAP_NAME; const map = new maplibregl.Map({ container: 'map', style: `https://maps.geo.${region}.amazonaws.com/maps/v0/maps/${mapName}/style-descriptor?key=${mapApiKey}`, center: [138.0000, 38.5000], zoom: baseZoom, maxZoom: 20 }); COMMAND_BLOCK: interface LayerConfig { name: string; iconPath: string; iconId: string; visible: boolean; } const layerConfigs: Record<string, LayerConfig> = { 'gyouza': { name: '餃子の山岡家', iconPath: 'icons/gyouza.png', iconId: 'gyouza-icon', visible: true }, 'miso': { name: '味噌ラーメン山岡家', iconPath: 'icons/miso.png', iconId: 'miso-icon', visible: true }, 'niboshi': { name: '煮干しラーメン山岡家', iconPath: 'icons/niboshi.png', iconId: 'niboshi-icon', visible: true }, 'yama': { name: 'ラーメン山岡家', iconPath: 'icons/yama.png', iconId: 'yama-icon', visible: true } }; Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: interface LayerConfig { name: string; iconPath: string; iconId: string; visible: boolean; } const layerConfigs: Record<string, LayerConfig> = { 'gyouza': { name: '餃子の山岡家', iconPath: 'icons/gyouza.png', iconId: 'gyouza-icon', visible: true }, 'miso': { name: '味噌ラーメン山岡家', iconPath: 'icons/miso.png', iconId: 'miso-icon', visible: true }, 'niboshi': { name: '煮干しラーメン山岡家', iconPath: 'icons/niboshi.png', iconId: 'niboshi-icon', visible: true }, 'yama': { name: 'ラーメン山岡家', iconPath: 'icons/yama.png', iconId: 'yama-icon', visible: true } }; COMMAND_BLOCK: interface LayerConfig { name: string; iconPath: string; iconId: string; visible: boolean; } const layerConfigs: Record<string, LayerConfig> = { 'gyouza': { name: '餃子の山岡家', iconPath: 'icons/gyouza.png', iconId: 'gyouza-icon', visible: true }, 'miso': { name: '味噌ラーメン山岡家', iconPath: 'icons/miso.png', iconId: 'miso-icon', visible: true }, 'niboshi': { name: '煮干しラーメン山岡家', iconPath: 'icons/niboshi.png', iconId: 'niboshi-icon', visible: true }, 'yama': { name: 'ラーメン山岡家', iconPath: 'icons/yama.png', iconId: 'yama-icon', visible: true } }; CODE_BLOCK: function addGeoJsonLayer(id: string, config: LayerConfig, data: GeoJSONData): void { map.addSource(id, { type: 'geojson', data: data }); map.addLayer({ id: id, type: 'symbol', source: id, layout: { 'icon-image': config.iconId, 'icon-size': [ 'interpolate', ['linear'], ['zoom'], 6, baseIconSize * 0.5, 10, baseIconSize * 0.6, 14, baseIconSize * 0.7, 18, baseIconSize * 0.8 ], 'icon-allow-overlap': true, 'icon-ignore-placement': false, }, paint: { 'icon-opacity': 1.0, } }); } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: function addGeoJsonLayer(id: string, config: LayerConfig, data: GeoJSONData): void { map.addSource(id, { type: 'geojson', data: data }); map.addLayer({ id: id, type: 'symbol', source: id, layout: { 'icon-image': config.iconId, 'icon-size': [ 'interpolate', ['linear'], ['zoom'], 6, baseIconSize * 0.5, 10, baseIconSize * 0.6, 14, baseIconSize * 0.7, 18, baseIconSize * 0.8 ], 'icon-allow-overlap': true, 'icon-ignore-placement': false, }, paint: { 'icon-opacity': 1.0, } }); } CODE_BLOCK: function addGeoJsonLayer(id: string, config: LayerConfig, data: GeoJSONData): void { map.addSource(id, { type: 'geojson', data: data }); map.addLayer({ id: id, type: 'symbol', source: id, layout: { 'icon-image': config.iconId, 'icon-size': [ 'interpolate', ['linear'], ['zoom'], 6, baseIconSize * 0.5, 10, baseIconSize * 0.6, 14, baseIconSize * 0.7, 18, baseIconSize * 0.8 ], 'icon-allow-overlap': true, 'icon-ignore-placement': false, }, paint: { 'icon-opacity': 1.0, } }); } COMMAND_BLOCK: function createPopupContent(props: StoreProperties): string { const contentParts: string[] = []; if (props.store_name) { contentParts.push(`<h3>${props.store_name}</h3>`); } const details: string[] = []; if (props.address) { details.push(`<strong>住所:</strong> ${props.address}`); } if (props.phone_number) { details.push(`<strong>電話:</strong> <a href="tel:${props.phone_number}">${props.phone_number}</a>`); } // ... } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: function createPopupContent(props: StoreProperties): string { const contentParts: string[] = []; if (props.store_name) { contentParts.push(`<h3>${props.store_name}</h3>`); } const details: string[] = []; if (props.address) { details.push(`<strong>住所:</strong> ${props.address}`); } if (props.phone_number) { details.push(`<strong>電話:</strong> <a href="tel:${props.phone_number}">${props.phone_number}</a>`); } // ... } COMMAND_BLOCK: function createPopupContent(props: StoreProperties): string { const contentParts: string[] = []; if (props.store_name) { contentParts.push(`<h3>${props.store_name}</h3>`); } const details: string[] = []; if (props.address) { details.push(`<strong>住所:</strong> ${props.address}`); } if (props.phone_number) { details.push(`<strong>電話:</strong> <a href="tel:${props.phone_number}">${props.phone_number}</a>`); } // ... } CODE_BLOCK: const overLayers = { 'yama': 'ラーメン山岡家', 'niboshi': '煮干しラーメン山岡家', 'miso': '味噌ラーメン山岡家', 'gyouza': '餃子の山岡家', }; const opacityControl = new OpacityControl({ overLayers: overLayers, opacityControl: false }); map.addControl(opacityControl, 'bottom-left'); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: const overLayers = { 'yama': 'ラーメン山岡家', 'niboshi': '煮干しラーメン山岡家', 'miso': '味噌ラーメン山岡家', 'gyouza': '餃子の山岡家', }; const opacityControl = new OpacityControl({ overLayers: overLayers, opacityControl: false }); map.addControl(opacityControl, 'bottom-left'); CODE_BLOCK: const overLayers = { 'yama': 'ラーメン山岡家', 'niboshi': '煮干しラーメン山岡家', 'miso': '味噌ラーメン山岡家', 'gyouza': '餃子の山岡家', }; const opacityControl = new OpacityControl({ overLayers: overLayers, opacityControl: false }); map.addControl(opacityControl, 'bottom-left'); - iOS (Safari): Share button → “Add to Home Screen.” - Android (Chrome): Menu → “Add to Home Screen.” - Scraping: Playwright - Data Processing: pandas - DMS→DD Conversion: geopy - node v24.4.1 - npm v11.4.2