DEV Community

Cover image for Building the Yamaokaya Map (Unofficial)
Yasunori Kirimoto for MIERUNE

Posted on

Building the Yamaokaya Map (Unofficial)

About Yamaokaya Map (Unofficial)

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.

img

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.

How to add:

  • iOS (Safari): Share button → “Add to Home Screen.”
  • Android (Chrome): Menu → “Add to Home Screen.”

Ramen Yamaokaya Store Types

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.

Advance Preparation

Contacting the Official Website

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.

img

Data Acquisition and Processing

This time, I'll use Python for scraping. I'll combine Playwright, pandas, and geopy to acquire and process the data.

  • Scraping: Playwright
  • Data Processing: pandas
  • DMS→DD Conversion: geopy
yamaokaya-data
└── script
    ├── scrape_yamaokaya.py
    ├── latlon_yamaokaya.py
    ├── column_yamaokaya.py
    ├── csv2geojson.py
Enter fullscreen mode Exit fullscreen mode

Map Application

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

  • node v24.4.1
  • npm v11.4.2
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

Install the package

npm install
Enter fullscreen mode Exit fullscreen mode

Publishing Settings in Amplify Gen2

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

Data Acquisition and Processing

Scraping

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

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

DMS→DD Conversion

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

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

Column Name Change

Before converting the data to GeoJSON, I change Japanese column names to English.

Example of column name change

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

CSV to GeoJSON Conversion

Finally, I convert the CSV to GeoJSON format. Files are output separately for each store type.

Example of CSV to GeoJSON conversion

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

GeoJSON output result

{
    "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

Creating the Map Application

Setting the Background Map

For this project, I use MapLibre GL JS as the map library and Amazon Location Service for the background map.

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

Layer Configuration

I set up layers for each store type and assign custom icons to them.

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

Adding GeoJSON Layers

I add the GeoJSON data as layers. I configure the icon size to change based on the zoom level.

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

Implementing Popups

Clicking a store icon displays the store information in a popup. It shows the address, phone number, business hours, parking information, seating types, etc.

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

Layer Toggling

Implemented layer toggling (show/hide) using maplibre-gl-opacity.

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

Summary

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.

img

Please use this when searching for a nearby store or looking for Ramen Yamaokaya while traveling!

Top comments (0)