編集の要約なし |
編集の要約なし |
||
| 1,011行目: | 1,011行目: | ||
initStallCompare(); | initStallCompare(); | ||
} | |||
); | |||
} ); | |||
/* ======================================== | |||
* 屋台比較ページ | |||
* | |||
* localStorage: | |||
* matsuriWikiCompareStalls | |||
* | |||
* 2~4店舗をCargoから取得して | |||
* 横並び比較表を自動生成 | |||
* ======================================== */ | |||
mw.loader.using( [ | |||
'mediawiki.storage', | |||
'mediawiki.api', | |||
'mediawiki.util' | |||
] ).then( function () { | |||
'use strict'; | |||
const compareRoot = | |||
document.getElementById( | |||
'stall-compare-page' | |||
); | |||
/* | |||
* 屋台比較ページ以外では | |||
* 何もしない | |||
*/ | |||
if ( !compareRoot ) { | |||
return; | |||
} | |||
const STORAGE_KEY = | |||
'matsuriWikiCompareStalls'; | |||
const MIN_COMPARE = 2; | |||
const MAX_COMPARE = 4; | |||
const api = | |||
new mw.Api(); | |||
/* ===================================== | |||
* 比較年 | |||
* | |||
* ?year=2026 があれば使用 | |||
* 無ければ現在年 | |||
* ===================================== */ | |||
function getCompareYear() { | |||
const params = | |||
new URLSearchParams( | |||
window.location.search | |||
); | |||
const yearParam = | |||
params.get( 'year' ); | |||
if ( | |||
yearParam && | |||
/^\d{4}$/.test( yearParam ) | |||
) { | |||
const year = | |||
Number( yearParam ); | |||
if ( | |||
year >= 2000 && | |||
year <= 2100 | |||
) { | |||
return year; | |||
} | |||
} | |||
return new Date().getFullYear(); | |||
} | |||
const compareYear = | |||
getCompareYear(); | |||
/* ===================================== | |||
* localStorageからstall_id取得 | |||
* ===================================== */ | |||
function getCompareStallIds() { | |||
const raw = | |||
mw.storage.get( | |||
STORAGE_KEY | |||
); | |||
if ( !raw ) { | |||
return []; | |||
} | |||
try { | |||
const ids = | |||
JSON.parse( raw ); | |||
if ( !Array.isArray( ids ) ) { | |||
return []; | |||
} | |||
return [ ...new Set( | |||
ids | |||
.map( String ) | |||
.filter( | |||
function ( id ) { | |||
return /^\d+$/.test( | |||
id | |||
); | |||
} | |||
) | |||
) ].slice( | |||
0, | |||
MAX_COMPARE | |||
); | |||
} catch ( e ) { | |||
return []; | |||
} | |||
} | |||
/* ===================================== | |||
* Cargo API共通処理 | |||
* ===================================== */ | |||
function cargoQuery( | |||
table, | |||
fields, | |||
where, | |||
limit | |||
) { | |||
const params = { | |||
action: 'cargoquery', | |||
tables: table, | |||
fields: fields, | |||
limit: limit || 100, | |||
format: 'json' | |||
}; | |||
if ( where ) { | |||
params.where = where; | |||
} | |||
return api.get( | |||
params | |||
).then( | |||
function ( data ) { | |||
if ( | |||
!data || | |||
!Array.isArray( | |||
data.cargoquery | |||
) | |||
) { | |||
return []; | |||
} | |||
return data.cargoquery.map( | |||
function ( item ) { | |||
return ( | |||
item.title || | |||
item | |||
); | |||
} | |||
); | |||
} | |||
); | |||
} | |||
/* ===================================== | |||
* ID配列をCargo IN句にする | |||
* ===================================== */ | |||
function makeInClause( ids ) { | |||
return ids | |||
.filter( | |||
function ( id ) { | |||
return /^\d+$/.test( | |||
String( id ) | |||
); | |||
} | |||
) | |||
.join( ',' ); | |||
} | |||
/* ===================================== | |||
* 重複削除 | |||
* ===================================== */ | |||
function uniqueIds( values ) { | |||
return [ | |||
...new Set( | |||
values | |||
.map( String ) | |||
.filter( | |||
function ( value ) { | |||
return ( | |||
value && | |||
/^\d+$/.test( | |||
value | |||
) | |||
); | |||
} | |||
) | |||
) | |||
]; | |||
} | |||
/* ===================================== | |||
* Object Map作成 | |||
* ===================================== */ | |||
function mapBy( | |||
rows, | |||
key | |||
) { | |||
const result = {}; | |||
rows.forEach( | |||
function ( row ) { | |||
if ( | |||
row[ key ] === | |||
undefined | |||
) { | |||
return; | |||
} | |||
result[ | |||
String( | |||
row[ key ] | |||
) | |||
] = row; | |||
} | |||
); | |||
return result; | |||
} | |||
/* ===================================== | |||
* 表示用関数 | |||
* ===================================== */ | |||
function textOrDash( value ) { | |||
if ( | |||
value === undefined || | |||
value === null || | |||
value === '' | |||
) { | |||
return '―'; | |||
} | |||
return String( value ); | |||
} | |||
function cleanNumber( value ) { | |||
const number = | |||
Number( value ); | |||
if ( | |||
!Number.isFinite( | |||
number | |||
) | |||
) { | |||
return ''; | |||
} | |||
if ( | |||
Number.isInteger( | |||
number | |||
) | |||
) { | |||
return String( | |||
number | |||
); | |||
} | |||
return String( | |||
Math.round( | |||
number * 100 | |||
) / 100 | |||
); | |||
} | |||
function formatHours( | |||
placement | |||
) { | |||
if ( !placement ) { | |||
return '―'; | |||
} | |||
const open = | |||
placement.opening_time || ''; | |||
const close = | |||
placement.closing_time || ''; | |||
if ( | |||
open && | |||
close | |||
) { | |||
return ( | |||
open + | |||
'~' + | |||
close | |||
); | |||
} | |||
if ( open ) { | |||
return open + '~'; | |||
} | |||
if ( close ) { | |||
return '~' + close; | |||
} | |||
if ( | |||
placement.hours_note | |||
) { | |||
return placement.hours_note; | |||
} | |||
return '未確認'; | |||
} | |||
function formatPositionStatus( | |||
status | |||
) { | |||
switch ( status ) { | |||
case 'exact': | |||
return '正確な位置'; | |||
case 'approximate': | |||
return 'おおよその位置'; | |||
case 'test': | |||
return 'テスト位置'; | |||
default: | |||
return '位置未確認'; | |||
} | |||
} | |||
function formatVerification( | |||
status | |||
) { | |||
switch ( status ) { | |||
case 'verified': | |||
return '確認済み'; | |||
case 'partially_verified': | |||
return '一部確認済み'; | |||
case 'outdated': | |||
return '情報が古い'; | |||
default: | |||
return '未確認'; | |||
} | |||
} | |||
function formatAvailability( | |||
status | |||
) { | |||
switch ( status ) { | |||
case 'available': | |||
return '販売あり'; | |||
case 'unavailable': | |||
return '販売なし'; | |||
default: | |||
return '未確認'; | |||
} | |||
} | |||
/* ===================================== | |||
* 1単位あたり価格 | |||
* ===================================== */ | |||
function getUnitPrice( | |||
offering | |||
) { | |||
if ( !offering ) { | |||
return '―'; | |||
} | |||
const price = | |||
Number( | |||
offering.price | |||
); | |||
const quantity = | |||
Number( | |||
offering.serving_quantity | |||
); | |||
if ( | |||
!Number.isFinite( | |||
price | |||
) || | |||
!Number.isFinite( | |||
quantity | |||
) || | |||
quantity <= 0 | |||
) { | |||
return '―'; | |||
} | |||
const unitPrice = | |||
Math.round( | |||
( | |||
price / | |||
quantity | |||
) * 100 | |||
) / 100; | |||
const unit = | |||
offering.serving_unit || | |||
'単位'; | |||
return ( | |||
unitPrice + | |||
'円/' + | |||
unit | |||
); | |||
} | |||
/* ===================================== | |||
* DOM helper | |||
* ===================================== */ | |||
function createTextCell( | |||
tagName, | |||
text | |||
) { | |||
const cell = | |||
document.createElement( | |||
tagName | |||
); | |||
cell.textContent = | |||
text; | |||
return cell; | |||
} | |||
/* ===================================== | |||
* Menu表示 | |||
* ===================================== */ | |||
function createMenuList( | |||
menuData | |||
) { | |||
const container = | |||
document.createElement( | |||
'div' | |||
); | |||
container.className = | |||
'stall-compare-menu-list'; | |||
if ( | |||
!menuData || | |||
menuData.length === 0 | |||
) { | |||
container.textContent = | |||
'メニュー未登録'; | |||
return container; | |||
} | |||
menuData.forEach( | |||
function ( item ) { | |||
const menu = | |||
document.createElement( | |||
'div' | |||
); | |||
menu.className = | |||
'stall-compare-menu-item'; | |||
const name = | |||
document.createElement( | |||
'strong' | |||
); | |||
name.className = | |||
'stall-compare-menu-name'; | |||
name.textContent = | |||
item.menuName || | |||
'商品'; | |||
const price = | |||
document.createElement( | |||
'div' | |||
); | |||
price.textContent = | |||
item.price | |||
? item.price + '円' | |||
: '価格未確認'; | |||
const serving = | |||
document.createElement( | |||
'div' | |||
); | |||
if ( | |||
item.servingQuantity | |||
) { | |||
serving.textContent = | |||
'内容量:' + | |||
item.servingQuantity + | |||
( | |||
item.servingUnit || | |||
'' | |||
); | |||
} else { | |||
serving.textContent = | |||
'内容量:未確認'; | |||
} | |||
const perUnit = | |||
document.createElement( | |||
'div' | |||
); | |||
perUnit.textContent = | |||
'1単位あたり:' + | |||
item.unitPrice; | |||
const availability = | |||
document.createElement( | |||
'div' | |||
); | |||
availability.textContent = | |||
'販売状況:' + | |||
item.availability; | |||
menu.appendChild( | |||
name | |||
); | |||
menu.appendChild( | |||
price | |||
); | |||
menu.appendChild( | |||
serving | |||
); | |||
menu.appendChild( | |||
perUnit | |||
); | |||
menu.appendChild( | |||
availability | |||
); | |||
container.appendChild( | |||
menu | |||
); | |||
} | |||
); | |||
return container; | |||
} | |||
/* ===================================== | |||
* 比較表生成 | |||
* ===================================== */ | |||
function renderComparison( | |||
compareData | |||
) { | |||
compareRoot.innerHTML = ''; | |||
/* | |||
* 年表示 | |||
*/ | |||
const heading = | |||
document.createElement( | |||
'h2' | |||
); | |||
heading.textContent = | |||
compareYear + | |||
'年の屋台比較'; | |||
compareRoot.appendChild( | |||
heading | |||
); | |||
/* | |||
* 横スクロール用 | |||
*/ | |||
const wrapper = | |||
document.createElement( | |||
'div' | |||
); | |||
wrapper.className = | |||
'stall-compare-table-wrapper'; | |||
const table = | |||
document.createElement( | |||
'table' | |||
); | |||
table.className = | |||
'stall-compare-table'; | |||
/* ============================== | |||
* ヘッダー | |||
* ============================== */ | |||
const thead = | |||
document.createElement( | |||
'thead' | |||
); | |||
const headerRow = | |||
document.createElement( | |||
'tr' | |||
); | |||
headerRow.appendChild( | |||
createTextCell( | |||
'th', | |||
'比較項目' | |||
) | |||
); | |||
compareData.forEach( | |||
function ( data ) { | |||
const th = | |||
document.createElement( | |||
'th' | |||
); | |||
if ( | |||
data.stall && | |||
data.stall.page_name | |||
) { | |||
const link = | |||
document.createElement( | |||
'a' | |||
); | |||
link.href = | |||
mw.util.getUrl( | |||
data.stall.page_name | |||
); | |||
link.textContent = | |||
data.stall.stall_name || | |||
'屋台'; | |||
th.appendChild( | |||
link | |||
); | |||
} else { | |||
th.textContent = | |||
data.stall | |||
? data.stall.stall_name | |||
: '屋台'; | |||
} | |||
headerRow.appendChild( | |||
th | |||
); | |||
} | |||
); | |||
thead.appendChild( | |||
headerRow | |||
); | |||
table.appendChild( | |||
thead | |||
); | |||
const tbody = | |||
document.createElement( | |||
'tbody' | |||
); | |||
/* ============================== | |||
* 普通の文字列行 | |||
* ============================== */ | |||
function addRow( | |||
label, | |||
getter | |||
) { | |||
const tr = | |||
document.createElement( | |||
'tr' | |||
); | |||
const labelCell = | |||
createTextCell( | |||
'th', | |||
label | |||
); | |||
labelCell.scope = | |||
'row'; | |||
tr.appendChild( | |||
labelCell | |||
); | |||
compareData.forEach( | |||
function ( data ) { | |||
const td = | |||
createTextCell( | |||
'td', | |||
textOrDash( | |||
getter( | |||
data | |||
) | |||
) | |||
); | |||
tr.appendChild( | |||
td | |||
); | |||
} | |||
); | |||
tbody.appendChild( | |||
tr | |||
); | |||
} | |||
addRow( | |||
'祭り', | |||
function ( data ) { | |||
return data.festival | |||
? data.festival.festival_name | |||
: '―'; | |||
} | |||
); | |||
addRow( | |||
'会場', | |||
function ( data ) { | |||
return data.venue | |||
? data.venue.venue_name | |||
: '―'; | |||
} | |||
); | |||
addRow( | |||
'地域', | |||
function ( data ) { | |||
return data.area | |||
? data.area.area_name | |||
: '―'; | |||
} | |||
); | |||
addRow( | |||
'カテゴリ', | |||
function ( data ) { | |||
return data.stall | |||
? data.stall.category | |||
: '―'; | |||
} | |||
); | |||
addRow( | |||
'出店場所', | |||
function ( data ) { | |||
return data.placement | |||
? data.placement.location_note | |||
: '―'; | |||
} | |||
); | |||
addRow( | |||
'営業時間', | |||
function ( data ) { | |||
return formatHours( | |||
data.placement | |||
); | |||
} | |||
); | |||
addRow( | |||
'位置情報', | |||
function ( data ) { | |||
return data.placement | |||
? formatPositionStatus( | |||
data.placement | |||
.position_status | |||
) | |||
: '―'; | |||
} | |||
); | |||
addRow( | |||
'確認状態', | |||
function ( data ) { | |||
return data.placement | |||
? formatVerification( | |||
data.placement | |||
.verification_status | |||
) | |||
: '―'; | |||
} | |||
); | |||
/* ============================== | |||
* Menu行 | |||
* ============================== */ | |||
const menuRow = | |||
document.createElement( | |||
'tr' | |||
); | |||
const menuLabel = | |||
createTextCell( | |||
'th', | |||
'メニュー' | |||
); | |||
menuLabel.scope = | |||
'row'; | |||
menuRow.appendChild( | |||
menuLabel | |||
); | |||
compareData.forEach( | |||
function ( data ) { | |||
const td = | |||
document.createElement( | |||
'td' | |||
); | |||
td.appendChild( | |||
createMenuList( | |||
data.menus | |||
) | |||
); | |||
menuRow.appendChild( | |||
td | |||
); | |||
} | |||
); | |||
tbody.appendChild( | |||
menuRow | |||
); | |||
table.appendChild( | |||
tbody | |||
); | |||
wrapper.appendChild( | |||
table | |||
); | |||
compareRoot.appendChild( | |||
wrapper | |||
); | |||
/* | |||
* 注意書き | |||
*/ | |||
const note = | |||
document.createElement( | |||
'p' | |||
); | |||
note.className = | |||
'stall-compare-note'; | |||
note.textContent = | |||
'価格・営業時間・出店位置は開催年によって変わる場合があります。確認状態もあわせて参照してください。'; | |||
compareRoot.appendChild( | |||
note | |||
); | |||
} | |||
/* ===================================== | |||
* エラー表示 | |||
* ===================================== */ | |||
function renderMessage( | |||
message | |||
) { | |||
compareRoot.innerHTML = ''; | |||
const p = | |||
document.createElement( | |||
'p' | |||
); | |||
p.className = | |||
'stall-compare-page-message'; | |||
p.textContent = | |||
message; | |||
compareRoot.appendChild( | |||
p | |||
); | |||
} | |||
/* ===================================== | |||
* データ取得開始 | |||
* ===================================== */ | |||
const stallIds = | |||
getCompareStallIds(); | |||
if ( | |||
stallIds.length < | |||
MIN_COMPARE | |||
) { | |||
renderMessage( | |||
'比較する屋台を2店舗以上選択してください。' | |||
); | |||
return; | |||
} | |||
/* | |||
* STEP 1 | |||
* | |||
* Stalls | |||
* FestivalStallPlacements | |||
*/ | |||
Promise.all( [ | |||
cargoQuery( | |||
'Stalls', | |||
'stall_id=stall_id,' + | |||
'name=stall_name,' + | |||
'category=category,' + | |||
'_pageName=page_name', | |||
'stall_id IN (' + | |||
makeInClause( | |||
stallIds | |||
) + | |||
')', | |||
MAX_COMPARE | |||
), | |||
cargoQuery( | |||
'FestivalStallPlacements', | |||
'placement_id=placement_id,' + | |||
'stall_id=stall_id,' + | |||
'festival_id=festival_id,' + | |||
'venue_id=venue_id,' + | |||
'year=year,' + | |||
'location_note=location_note,' + | |||
'opening_time=opening_time,' + | |||
'closing_time=closing_time,' + | |||
'hours_note=hours_note,' + | |||
'position_status=position_status,' + | |||
'verification_status=verification_status,' + | |||
'sort_order=sort_order', | |||
'stall_id IN (' + | |||
makeInClause( | |||
stallIds | |||
) + | |||
') AND year=' + | |||
compareYear, | |||
100 | |||
) | |||
] ).then( | |||
function ( firstResults ) { | |||
const stalls = | |||
firstResults[ 0 ]; | |||
const placements = | |||
firstResults[ 1 ]; | |||
/* | |||
* 同じstallに複数Placementがある場合 | |||
* sort_orderの小さいものを | |||
* 今回の比較対象にする | |||
* | |||
* 後でfestival_idまで比較条件に | |||
* 含める設計へ拡張可能 | |||
*/ | |||
const placementByStall = {}; | |||
placements | |||
.slice() | |||
.sort( | |||
function ( a, b ) { | |||
return ( | |||
Number( | |||
a.sort_order || 0 | |||
) - | |||
Number( | |||
b.sort_order || 0 | |||
) | |||
); | |||
} | |||
) | |||
.forEach( | |||
function ( | |||
placement | |||
) { | |||
const id = | |||
String( | |||
placement.stall_id | |||
); | |||
if ( | |||
!placementByStall[ | |||
id | |||
] | |||
) { | |||
placementByStall[ | |||
id | |||
] = | |||
placement; | |||
} | |||
} | |||
); | |||
const chosenPlacements = | |||
stallIds | |||
.map( | |||
function ( id ) { | |||
return ( | |||
placementByStall[ | |||
id | |||
] || | |||
null | |||
); | |||
} | |||
) | |||
.filter( | |||
Boolean | |||
); | |||
const festivalIds = | |||
uniqueIds( | |||
chosenPlacements.map( | |||
function ( row ) { | |||
return row.festival_id; | |||
} | |||
) | |||
); | |||
const venueIds = | |||
uniqueIds( | |||
chosenPlacements.map( | |||
function ( row ) { | |||
return row.venue_id; | |||
} | |||
) | |||
); | |||
const placementIds = | |||
uniqueIds( | |||
chosenPlacements.map( | |||
function ( row ) { | |||
return row.placement_id; | |||
} | |||
) | |||
); | |||
/* | |||
* STEP 2 | |||
* | |||
* Festivals | |||
* Venues | |||
* Offerings | |||
*/ | |||
return Promise.all( [ | |||
festivalIds.length | |||
? cargoQuery( | |||
'Festivals', | |||
'festival_id=festival_id,' + | |||
'name=festival_name,' + | |||
'_pageName=page_name', | |||
'festival_id IN (' + | |||
makeInClause( | |||
festivalIds | |||
) + | |||
')', | |||
100 | |||
) | |||
: Promise.resolve( | |||
[] | |||
), | |||
venueIds.length | |||
? cargoQuery( | |||
'Venues', | |||
'venue_id=venue_id,' + | |||
'name=venue_name,' + | |||
'area_id=area_id,' + | |||
'_pageName=page_name', | |||
'venue_id IN (' + | |||
makeInClause( | |||
venueIds | |||
) + | |||
')', | |||
100 | |||
) | |||
: Promise.resolve( | |||
[] | |||
), | |||
placementIds.length | |||
? cargoQuery( | |||
'FestivalStallMenuOfferings', | |||
'offering_id=offering_id,' + | |||
'placement_id=placement_id,' + | |||
'menu_item_id=menu_item_id,' + | |||
'price=price,' + | |||
'serving_quantity=serving_quantity,' + | |||
'serving_unit=serving_unit,' + | |||
'serving_note=serving_note,' + | |||
'availability=availability,' + | |||
'verification_status=verification_status,' + | |||
'sort_order=sort_order', | |||
'placement_id IN (' + | |||
makeInClause( | |||
placementIds | |||
) + | |||
')', | |||
100 | |||
) | |||
: Promise.resolve( | |||
[] | |||
) | |||
] ).then( | |||
function ( | |||
secondResults | |||
) { | |||
return { | |||
stalls: stalls, | |||
placementByStall: | |||
placementByStall, | |||
festivals: | |||
secondResults[ 0 ], | |||
venues: | |||
secondResults[ 1 ], | |||
offerings: | |||
secondResults[ 2 ] | |||
}; | |||
} | |||
); | |||
} | |||
).then( | |||
function ( data ) { | |||
const areaIds = | |||
uniqueIds( | |||
data.venues.map( | |||
function ( row ) { | |||
return row.area_id; | |||
} | |||
) | |||
); | |||
const menuItemIds = | |||
uniqueIds( | |||
data.offerings.map( | |||
function ( row ) { | |||
return row.menu_item_id; | |||
} | |||
) | |||
); | |||
/* | |||
* STEP 3 | |||
* | |||
* Areas | |||
* StallMenuItems | |||
*/ | |||
return Promise.all( [ | |||
areaIds.length | |||
? cargoQuery( | |||
'Areas', | |||
'area_id=area_id,' + | |||
'name=area_name,' + | |||
'_pageName=page_name', | |||
'area_id IN (' + | |||
makeInClause( | |||
areaIds | |||
) + | |||
')', | |||
100 | |||
) | |||
: Promise.resolve( | |||
[] | |||
), | |||
menuItemIds.length | |||
? cargoQuery( | |||
'StallMenuItems', | |||
'menu_item_id=menu_item_id,' + | |||
'stall_id=stall_id,' + | |||
'name=menu_name,' + | |||
'item_category=item_category,' + | |||
'sort_order=sort_order', | |||
'menu_item_id IN (' + | |||
makeInClause( | |||
menuItemIds | |||
) + | |||
')', | |||
100 | |||
) | |||
: Promise.resolve( | |||
[] | |||
) | |||
] ).then( | |||
function ( | |||
thirdResults | |||
) { | |||
data.areas = | |||
thirdResults[ 0 ]; | |||
data.menuItems = | |||
thirdResults[ 1 ]; | |||
return data; | |||
} | |||
); | |||
} | |||
).then( | |||
function ( data ) { | |||
/* ============================ | |||
* Map化 | |||
* ============================ */ | |||
const stallMap = | |||
mapBy( | |||
data.stalls, | |||
'stall_id' | |||
); | |||
const festivalMap = | |||
mapBy( | |||
data.festivals, | |||
'festival_id' | |||
); | |||
const venueMap = | |||
mapBy( | |||
data.venues, | |||
'venue_id' | |||
); | |||
const areaMap = | |||
mapBy( | |||
data.areas, | |||
'area_id' | |||
); | |||
const menuMap = | |||
mapBy( | |||
data.menuItems, | |||
'menu_item_id' | |||
); | |||
/* ============================ | |||
* OfferingをPlacementごとに | |||
* ============================ */ | |||
const offeringsByPlacement = | |||
{}; | |||
data.offerings | |||
.slice() | |||
.sort( | |||
function ( a, b ) { | |||
return ( | |||
Number( | |||
a.sort_order || 0 | |||
) - | |||
Number( | |||
b.sort_order || 0 | |||
) | |||
); | |||
} | |||
) | |||
.forEach( | |||
function ( | |||
offering | |||
) { | |||
const placementId = | |||
String( | |||
offering | |||
.placement_id | |||
); | |||
if ( | |||
!offeringsByPlacement[ | |||
placementId | |||
] | |||
) { | |||
offeringsByPlacement[ | |||
placementId | |||
] = []; | |||
} | |||
offeringsByPlacement[ | |||
placementId | |||
].push( | |||
offering | |||
); | |||
} | |||
); | |||
/* ============================ | |||
* 比較データを | |||
* localStorage順に生成 | |||
* ============================ */ | |||
const compareData = | |||
stallIds.map( | |||
function ( stallId ) { | |||
const stall = | |||
stallMap[ | |||
stallId | |||
] || null; | |||
const placement = | |||
data | |||
.placementByStall[ | |||
stallId | |||
] || null; | |||
let festival = null; | |||
let venue = null; | |||
let area = null; | |||
let menus = []; | |||
if ( placement ) { | |||
festival = | |||
festivalMap[ | |||
String( | |||
placement | |||
.festival_id | |||
) | |||
] || null; | |||
venue = | |||
venueMap[ | |||
String( | |||
placement | |||
.venue_id | |||
) | |||
] || null; | |||
if ( | |||
venue && | |||
venue.area_id | |||
) { | |||
area = | |||
areaMap[ | |||
String( | |||
venue | |||
.area_id | |||
) | |||
] || null; | |||
} | |||
const offerings = | |||
offeringsByPlacement[ | |||
String( | |||
placement | |||
.placement_id | |||
) | |||
] || []; | |||
menus = | |||
offerings.map( | |||
function ( | |||
offering | |||
) { | |||
const menu = | |||
menuMap[ | |||
String( | |||
offering | |||
.menu_item_id | |||
) | |||
] || {}; | |||
return { | |||
menuName: | |||
menu.menu_name || | |||
'商品', | |||
category: | |||
menu.item_category || | |||
'', | |||
price: | |||
cleanNumber( | |||
offering.price | |||
), | |||
servingQuantity: | |||
cleanNumber( | |||
offering | |||
.serving_quantity | |||
), | |||
servingUnit: | |||
offering | |||
.serving_unit || | |||
'', | |||
servingNote: | |||
offering | |||
.serving_note || | |||
'', | |||
unitPrice: | |||
getUnitPrice( | |||
offering | |||
), | |||
availability: | |||
formatAvailability( | |||
offering | |||
.availability | |||
), | |||
verification: | |||
formatVerification( | |||
offering | |||
.verification_status | |||
) | |||
}; | |||
} | |||
); | |||
} | |||
return { | |||
stallId: stallId, | |||
stall: stall, | |||
placement: | |||
placement, | |||
festival: | |||
festival, | |||
venue: | |||
venue, | |||
area: | |||
area, | |||
menus: | |||
menus | |||
}; | |||
} | |||
); | |||
renderComparison( | |||
compareData | |||
); | |||
} | |||
).catch( | |||
function ( error ) { | |||
console.error( | |||
'屋台比較データ取得エラー:', | |||
error | |||
); | |||
renderMessage( | |||
'比較データの取得中にエラーが発生しました。' | |||
); | |||
} | } | ||
2026年8月11日 (火) 21:53時点における版
/* ========================================
* 屋台比較
*
* ・最大4店舗
* ・localStorageにはstall_idだけ保存
* ・屋台名はCargo APIから取得
* ・画面下部に固定比較トレイを表示
* ======================================== */
mw.loader.using( [
'mediawiki.storage',
'mediawiki.api',
'mediawiki.util'
] ).then( function () {
'use strict';
const STORAGE_KEY = 'matsuriWikiCompareStalls';
const MAX_COMPARE = 4;
const api = new mw.Api();
/*
* このページを表示している間だけ使う
* 屋台名キャッシュ
*/
const stallNameCache = {};
/* =====================================
* localStorage
* ===================================== */
function getCompareStalls() {
const raw = mw.storage.get( STORAGE_KEY );
if ( !raw ) {
return [];
}
try {
const ids = JSON.parse( raw );
if ( !Array.isArray( ids ) ) {
return [];
}
/*
* 数字だけ許可
* localStorage改変対策
*/
return [ ...new Set(
ids
.map( String )
.filter( function ( id ) {
return /^\d+$/.test( id );
} )
) ].slice( 0, MAX_COMPARE );
} catch ( e ) {
return [];
}
}
function saveCompareStalls( ids ) {
mw.storage.set(
STORAGE_KEY,
JSON.stringify( ids )
);
}
/* =====================================
* Cargoから屋台名取得
* ===================================== */
function fetchStallNames( ids ) {
if ( ids.length === 0 ) {
return Promise.resolve( {} );
}
/*
* すでに取得済みのID
*/
const result = {};
const missingIds = [];
ids.forEach( function ( id ) {
if ( stallNameCache[ id ] ) {
result[ id ] =
stallNameCache[ id ];
} else {
missingIds.push( id );
}
} );
/*
* 全部キャッシュ済み
*/
if ( missingIds.length === 0 ) {
return Promise.resolve( result );
}
/*
* 数字だけにしてあるため
* IN (1,2,3) として安全にCargoへ渡す
*/
const where =
'stall_id IN (' +
missingIds.join( ',' ) +
')';
return api.get( {
action: 'cargoquery',
tables: 'Stalls',
fields:
'stall_id=stall_id,' +
'name=stall_name,' +
'_pageName=page_name',
where: where,
limit: MAX_COMPARE,
format: 'json'
} ).then( function ( data ) {
if (
!data ||
!Array.isArray( data.cargoquery )
) {
return result;
}
data.cargoquery.forEach( function ( item ) {
/*
* Cargo APIは通常
*
* {
* title: {
* stall_id: "...",
* stall_name: "..."
* }
* }
*
* の形式
*/
const row =
item.title || item;
if (
!row ||
row.stall_id === undefined
) {
return;
}
const id =
String( row.stall_id );
const info = {
id: id,
name:
row.stall_name ||
'屋台ID ' + id,
page:
row.page_name || ''
};
stallNameCache[ id ] = info;
result[ id ] = info;
} );
return result;
} ).catch( function () {
/*
* APIエラーでも
* 比較機能そのものは壊さない
*/
missingIds.forEach( function ( id ) {
result[ id ] = {
id: id,
name: '屋台ID ' + id,
page: ''
};
} );
return result;
} );
}
/* =====================================
* 詳細ページ側メッセージ
* ===================================== */
function showMessage(
control,
message,
isError
) {
const element =
control.querySelector(
'.stall-compare-message'
);
if ( !element ) {
return;
}
element.textContent =
message;
element.classList.toggle(
'stall-compare-message-error',
Boolean( isError )
);
}
/* =====================================
* 詳細ページ側button生成
* ===================================== */
function createCompareButtons() {
document
.querySelectorAll(
'.stall-compare-placeholder'
)
.forEach( function ( placeholder ) {
if (
placeholder.dataset.initialized ===
'1'
) {
return;
}
const prefix =
'stall-compare-placeholder-';
if (
!placeholder.id.startsWith(
prefix
)
) {
return;
}
const stallId =
placeholder.id.substring(
prefix.length
);
if (
!/^\d+$/.test( stallId )
) {
return;
}
const button =
document.createElement(
'button'
);
button.type =
'button';
button.className =
'stall-compare-button';
button.dataset.stallId =
stallId;
button.setAttribute(
'aria-pressed',
'false'
);
button.textContent =
'比較に追加';
placeholder.appendChild(
button
);
placeholder.dataset.initialized =
'1';
} );
}
/* =====================================
* 詳細ページ側button状態
* ===================================== */
function updateCompareButtons() {
const ids =
getCompareStalls();
document
.querySelectorAll(
'.stall-compare-button'
)
.forEach( function ( button ) {
const stallId =
String(
button.dataset.stallId || ''
);
const selected =
ids.includes( stallId );
if ( selected ) {
button.textContent =
'比較から外す';
button.classList.add(
'stall-compare-button-selected'
);
button.setAttribute(
'aria-pressed',
'true'
);
} else {
button.textContent =
'比較に追加';
button.classList.remove(
'stall-compare-button-selected'
);
button.setAttribute(
'aria-pressed',
'false'
);
}
} );
}
/* =====================================
* 固定比較トレイを生成
* ===================================== */
function createCompareTray() {
if (
document.getElementById(
'stall-compare-tray'
)
) {
return;
}
const tray =
document.createElement( 'div' );
tray.id =
'stall-compare-tray';
tray.className =
'stall-compare-tray';
tray.setAttribute(
'aria-live',
'polite'
);
/*
* ヘッダー
*/
const header =
document.createElement( 'div' );
header.className =
'stall-compare-tray-header';
const title =
document.createElement( 'strong' );
title.className =
'stall-compare-tray-title';
title.textContent =
'比較候補';
const count =
document.createElement( 'span' );
count.className =
'stall-compare-tray-count';
header.appendChild( title );
header.appendChild( count );
/*
* 屋台一覧
*/
const items =
document.createElement( 'div' );
items.className =
'stall-compare-tray-items';
/*
* アクション部分
*/
const actions =
document.createElement( 'div' );
actions.className =
'stall-compare-tray-actions';
const clearButton =
document.createElement( 'button' );
clearButton.type =
'button';
clearButton.className =
'stall-compare-clear';
clearButton.textContent =
'すべて外す';
const compareLink =
document.createElement( 'a' );
compareLink.className =
'stall-compare-open';
compareLink.href =
mw.util.getUrl( '屋台比較' );
compareLink.textContent =
'比較する';
actions.appendChild(
clearButton
);
actions.appendChild(
compareLink
);
tray.appendChild(
header
);
tray.appendChild(
items
);
tray.appendChild(
actions
);
document.body.appendChild(
tray
);
}
/* =====================================
* 固定比較トレイ更新
* ===================================== */
function updateCompareTray() {
createCompareTray();
const tray =
document.getElementById(
'stall-compare-tray'
);
if ( !tray ) {
return;
}
const ids =
getCompareStalls();
const count =
tray.querySelector(
'.stall-compare-tray-count'
);
const items =
tray.querySelector(
'.stall-compare-tray-items'
);
const compareLink =
tray.querySelector(
'.stall-compare-open'
);
if ( count ) {
count.textContent =
ids.length +
' / ' +
MAX_COMPARE;
}
/*
* 0件ならトレイ非表示
*/
if ( ids.length === 0 ) {
tray.classList.remove(
'stall-compare-tray-visible'
);
if ( items ) {
items.innerHTML = '';
}
return;
}
tray.classList.add(
'stall-compare-tray-visible'
);
/*
* 2件未満では比較ページを無効化
*/
if ( compareLink ) {
if ( ids.length >= 2 ) {
compareLink.classList.remove(
'stall-compare-open-disabled'
);
compareLink.setAttribute(
'aria-disabled',
'false'
);
} else {
compareLink.classList.add(
'stall-compare-open-disabled'
);
compareLink.setAttribute(
'aria-disabled',
'true'
);
}
}
if ( !items ) {
return;
}
/*
* 読み込み中
*/
items.textContent =
'屋台情報を読み込み中…';
fetchStallNames(
ids
).then( function ( stallInfo ) {
/*
* 更新待ち中に選択内容が
* 変わった場合も現在値を優先
*/
const currentIds =
getCompareStalls();
items.innerHTML =
'';
currentIds.forEach(
function ( id ) {
const info =
stallInfo[ id ] || {
id: id,
name:
'屋台ID ' + id,
page: ''
};
const item =
document.createElement(
'div'
);
item.className =
'stall-compare-tray-item';
/*
* 屋台名
*/
let nameElement;
if ( info.page ) {
nameElement =
document.createElement(
'a'
);
nameElement.href =
mw.util.getUrl(
info.page
);
} else {
nameElement =
document.createElement(
'span'
);
}
nameElement.className =
'stall-compare-tray-item-name';
nameElement.textContent =
info.name;
/*
* × ボタン
*/
const removeButton =
document.createElement(
'button'
);
removeButton.type =
'button';
removeButton.className =
'stall-compare-tray-remove';
removeButton.dataset.stallId =
id;
removeButton.setAttribute(
'aria-label',
info.name +
'を比較候補から外す'
);
removeButton.textContent =
'×';
item.appendChild(
nameElement
);
item.appendChild(
removeButton
);
items.appendChild(
item
);
}
);
} );
}
/* =====================================
* 詳細ページ比較button
* ===================================== */
document.addEventListener(
'click',
function ( event ) {
const button =
event.target.closest(
'.stall-compare-button'
);
if ( !button ) {
return;
}
event.preventDefault();
const stallId =
String(
button.dataset.stallId || ''
);
if (
!/^\d+$/.test( stallId )
) {
return;
}
const control =
button.closest(
'.stall-compare-control'
);
let ids =
getCompareStalls();
const index =
ids.indexOf(
stallId
);
/*
* すでに選択済み
* → 外す
*/
if ( index !== -1 ) {
ids.splice(
index,
1
);
saveCompareStalls(
ids
);
updateCompareButtons();
updateCompareTray();
if ( control ) {
showMessage(
control,
'比較候補から外しました。',
false
);
}
return;
}
/*
* 4件上限
*/
if (
ids.length >=
MAX_COMPARE
) {
if ( control ) {
showMessage(
control,
'比較できる屋台は最大4店舗です。',
true
);
}
return;
}
ids.push(
stallId
);
saveCompareStalls(
ids
);
updateCompareButtons();
updateCompareTray();
if ( control ) {
showMessage(
control,
'比較候補に追加しました(' +
ids.length +
'/4)。',
false
);
}
}
);
/* =====================================
* トレイ × button
* ===================================== */
document.addEventListener(
'click',
function ( event ) {
const button =
event.target.closest(
'.stall-compare-tray-remove'
);
if ( !button ) {
return;
}
event.preventDefault();
const stallId =
String(
button.dataset.stallId || ''
);
let ids =
getCompareStalls();
ids =
ids.filter(
function ( id ) {
return id !== stallId;
}
);
saveCompareStalls(
ids
);
updateCompareButtons();
updateCompareTray();
}
);
/* =====================================
* 「すべて外す」
* ===================================== */
document.addEventListener(
'click',
function ( event ) {
const button =
event.target.closest(
'.stall-compare-clear'
);
if ( !button ) {
return;
}
event.preventDefault();
saveCompareStalls(
[]
);
updateCompareButtons();
updateCompareTray();
}
);
/* =====================================
* 1件時の「比較する」クリック防止
* ===================================== */
document.addEventListener(
'click',
function ( event ) {
const link =
event.target.closest(
'.stall-compare-open-disabled'
);
if ( !link ) {
return;
}
event.preventDefault();
}
);
/* =====================================
* 初期化
* ===================================== */
function initStallCompare() {
createCompareButtons();
createCompareTray();
updateCompareButtons();
updateCompareTray();
}
initStallCompare();
mw.hook(
'wikipage.content'
).add(
function () {
initStallCompare();
}
);
} );
/* ========================================
* 屋台比較ページ
*
* localStorage:
* matsuriWikiCompareStalls
*
* 2~4店舗をCargoから取得して
* 横並び比較表を自動生成
* ======================================== */
mw.loader.using( [
'mediawiki.storage',
'mediawiki.api',
'mediawiki.util'
] ).then( function () {
'use strict';
const compareRoot =
document.getElementById(
'stall-compare-page'
);
/*
* 屋台比較ページ以外では
* 何もしない
*/
if ( !compareRoot ) {
return;
}
const STORAGE_KEY =
'matsuriWikiCompareStalls';
const MIN_COMPARE = 2;
const MAX_COMPARE = 4;
const api =
new mw.Api();
/* =====================================
* 比較年
*
* ?year=2026 があれば使用
* 無ければ現在年
* ===================================== */
function getCompareYear() {
const params =
new URLSearchParams(
window.location.search
);
const yearParam =
params.get( 'year' );
if (
yearParam &&
/^\d{4}$/.test( yearParam )
) {
const year =
Number( yearParam );
if (
year >= 2000 &&
year <= 2100
) {
return year;
}
}
return new Date().getFullYear();
}
const compareYear =
getCompareYear();
/* =====================================
* localStorageからstall_id取得
* ===================================== */
function getCompareStallIds() {
const raw =
mw.storage.get(
STORAGE_KEY
);
if ( !raw ) {
return [];
}
try {
const ids =
JSON.parse( raw );
if ( !Array.isArray( ids ) ) {
return [];
}
return [ ...new Set(
ids
.map( String )
.filter(
function ( id ) {
return /^\d+$/.test(
id
);
}
)
) ].slice(
0,
MAX_COMPARE
);
} catch ( e ) {
return [];
}
}
/* =====================================
* Cargo API共通処理
* ===================================== */
function cargoQuery(
table,
fields,
where,
limit
) {
const params = {
action: 'cargoquery',
tables: table,
fields: fields,
limit: limit || 100,
format: 'json'
};
if ( where ) {
params.where = where;
}
return api.get(
params
).then(
function ( data ) {
if (
!data ||
!Array.isArray(
data.cargoquery
)
) {
return [];
}
return data.cargoquery.map(
function ( item ) {
return (
item.title ||
item
);
}
);
}
);
}
/* =====================================
* ID配列をCargo IN句にする
* ===================================== */
function makeInClause( ids ) {
return ids
.filter(
function ( id ) {
return /^\d+$/.test(
String( id )
);
}
)
.join( ',' );
}
/* =====================================
* 重複削除
* ===================================== */
function uniqueIds( values ) {
return [
...new Set(
values
.map( String )
.filter(
function ( value ) {
return (
value &&
/^\d+$/.test(
value
)
);
}
)
)
];
}
/* =====================================
* Object Map作成
* ===================================== */
function mapBy(
rows,
key
) {
const result = {};
rows.forEach(
function ( row ) {
if (
row[ key ] ===
undefined
) {
return;
}
result[
String(
row[ key ]
)
] = row;
}
);
return result;
}
/* =====================================
* 表示用関数
* ===================================== */
function textOrDash( value ) {
if (
value === undefined ||
value === null ||
value === ''
) {
return '―';
}
return String( value );
}
function cleanNumber( value ) {
const number =
Number( value );
if (
!Number.isFinite(
number
)
) {
return '';
}
if (
Number.isInteger(
number
)
) {
return String(
number
);
}
return String(
Math.round(
number * 100
) / 100
);
}
function formatHours(
placement
) {
if ( !placement ) {
return '―';
}
const open =
placement.opening_time || '';
const close =
placement.closing_time || '';
if (
open &&
close
) {
return (
open +
'~' +
close
);
}
if ( open ) {
return open + '~';
}
if ( close ) {
return '~' + close;
}
if (
placement.hours_note
) {
return placement.hours_note;
}
return '未確認';
}
function formatPositionStatus(
status
) {
switch ( status ) {
case 'exact':
return '正確な位置';
case 'approximate':
return 'おおよその位置';
case 'test':
return 'テスト位置';
default:
return '位置未確認';
}
}
function formatVerification(
status
) {
switch ( status ) {
case 'verified':
return '確認済み';
case 'partially_verified':
return '一部確認済み';
case 'outdated':
return '情報が古い';
default:
return '未確認';
}
}
function formatAvailability(
status
) {
switch ( status ) {
case 'available':
return '販売あり';
case 'unavailable':
return '販売なし';
default:
return '未確認';
}
}
/* =====================================
* 1単位あたり価格
* ===================================== */
function getUnitPrice(
offering
) {
if ( !offering ) {
return '―';
}
const price =
Number(
offering.price
);
const quantity =
Number(
offering.serving_quantity
);
if (
!Number.isFinite(
price
) ||
!Number.isFinite(
quantity
) ||
quantity <= 0
) {
return '―';
}
const unitPrice =
Math.round(
(
price /
quantity
) * 100
) / 100;
const unit =
offering.serving_unit ||
'単位';
return (
unitPrice +
'円/' +
unit
);
}
/* =====================================
* DOM helper
* ===================================== */
function createTextCell(
tagName,
text
) {
const cell =
document.createElement(
tagName
);
cell.textContent =
text;
return cell;
}
/* =====================================
* Menu表示
* ===================================== */
function createMenuList(
menuData
) {
const container =
document.createElement(
'div'
);
container.className =
'stall-compare-menu-list';
if (
!menuData ||
menuData.length === 0
) {
container.textContent =
'メニュー未登録';
return container;
}
menuData.forEach(
function ( item ) {
const menu =
document.createElement(
'div'
);
menu.className =
'stall-compare-menu-item';
const name =
document.createElement(
'strong'
);
name.className =
'stall-compare-menu-name';
name.textContent =
item.menuName ||
'商品';
const price =
document.createElement(
'div'
);
price.textContent =
item.price
? item.price + '円'
: '価格未確認';
const serving =
document.createElement(
'div'
);
if (
item.servingQuantity
) {
serving.textContent =
'内容量:' +
item.servingQuantity +
(
item.servingUnit ||
''
);
} else {
serving.textContent =
'内容量:未確認';
}
const perUnit =
document.createElement(
'div'
);
perUnit.textContent =
'1単位あたり:' +
item.unitPrice;
const availability =
document.createElement(
'div'
);
availability.textContent =
'販売状況:' +
item.availability;
menu.appendChild(
name
);
menu.appendChild(
price
);
menu.appendChild(
serving
);
menu.appendChild(
perUnit
);
menu.appendChild(
availability
);
container.appendChild(
menu
);
}
);
return container;
}
/* =====================================
* 比較表生成
* ===================================== */
function renderComparison(
compareData
) {
compareRoot.innerHTML = '';
/*
* 年表示
*/
const heading =
document.createElement(
'h2'
);
heading.textContent =
compareYear +
'年の屋台比較';
compareRoot.appendChild(
heading
);
/*
* 横スクロール用
*/
const wrapper =
document.createElement(
'div'
);
wrapper.className =
'stall-compare-table-wrapper';
const table =
document.createElement(
'table'
);
table.className =
'stall-compare-table';
/* ==============================
* ヘッダー
* ============================== */
const thead =
document.createElement(
'thead'
);
const headerRow =
document.createElement(
'tr'
);
headerRow.appendChild(
createTextCell(
'th',
'比較項目'
)
);
compareData.forEach(
function ( data ) {
const th =
document.createElement(
'th'
);
if (
data.stall &&
data.stall.page_name
) {
const link =
document.createElement(
'a'
);
link.href =
mw.util.getUrl(
data.stall.page_name
);
link.textContent =
data.stall.stall_name ||
'屋台';
th.appendChild(
link
);
} else {
th.textContent =
data.stall
? data.stall.stall_name
: '屋台';
}
headerRow.appendChild(
th
);
}
);
thead.appendChild(
headerRow
);
table.appendChild(
thead
);
const tbody =
document.createElement(
'tbody'
);
/* ==============================
* 普通の文字列行
* ============================== */
function addRow(
label,
getter
) {
const tr =
document.createElement(
'tr'
);
const labelCell =
createTextCell(
'th',
label
);
labelCell.scope =
'row';
tr.appendChild(
labelCell
);
compareData.forEach(
function ( data ) {
const td =
createTextCell(
'td',
textOrDash(
getter(
data
)
)
);
tr.appendChild(
td
);
}
);
tbody.appendChild(
tr
);
}
addRow(
'祭り',
function ( data ) {
return data.festival
? data.festival.festival_name
: '―';
}
);
addRow(
'会場',
function ( data ) {
return data.venue
? data.venue.venue_name
: '―';
}
);
addRow(
'地域',
function ( data ) {
return data.area
? data.area.area_name
: '―';
}
);
addRow(
'カテゴリ',
function ( data ) {
return data.stall
? data.stall.category
: '―';
}
);
addRow(
'出店場所',
function ( data ) {
return data.placement
? data.placement.location_note
: '―';
}
);
addRow(
'営業時間',
function ( data ) {
return formatHours(
data.placement
);
}
);
addRow(
'位置情報',
function ( data ) {
return data.placement
? formatPositionStatus(
data.placement
.position_status
)
: '―';
}
);
addRow(
'確認状態',
function ( data ) {
return data.placement
? formatVerification(
data.placement
.verification_status
)
: '―';
}
);
/* ==============================
* Menu行
* ============================== */
const menuRow =
document.createElement(
'tr'
);
const menuLabel =
createTextCell(
'th',
'メニュー'
);
menuLabel.scope =
'row';
menuRow.appendChild(
menuLabel
);
compareData.forEach(
function ( data ) {
const td =
document.createElement(
'td'
);
td.appendChild(
createMenuList(
data.menus
)
);
menuRow.appendChild(
td
);
}
);
tbody.appendChild(
menuRow
);
table.appendChild(
tbody
);
wrapper.appendChild(
table
);
compareRoot.appendChild(
wrapper
);
/*
* 注意書き
*/
const note =
document.createElement(
'p'
);
note.className =
'stall-compare-note';
note.textContent =
'価格・営業時間・出店位置は開催年によって変わる場合があります。確認状態もあわせて参照してください。';
compareRoot.appendChild(
note
);
}
/* =====================================
* エラー表示
* ===================================== */
function renderMessage(
message
) {
compareRoot.innerHTML = '';
const p =
document.createElement(
'p'
);
p.className =
'stall-compare-page-message';
p.textContent =
message;
compareRoot.appendChild(
p
);
}
/* =====================================
* データ取得開始
* ===================================== */
const stallIds =
getCompareStallIds();
if (
stallIds.length <
MIN_COMPARE
) {
renderMessage(
'比較する屋台を2店舗以上選択してください。'
);
return;
}
/*
* STEP 1
*
* Stalls
* FestivalStallPlacements
*/
Promise.all( [
cargoQuery(
'Stalls',
'stall_id=stall_id,' +
'name=stall_name,' +
'category=category,' +
'_pageName=page_name',
'stall_id IN (' +
makeInClause(
stallIds
) +
')',
MAX_COMPARE
),
cargoQuery(
'FestivalStallPlacements',
'placement_id=placement_id,' +
'stall_id=stall_id,' +
'festival_id=festival_id,' +
'venue_id=venue_id,' +
'year=year,' +
'location_note=location_note,' +
'opening_time=opening_time,' +
'closing_time=closing_time,' +
'hours_note=hours_note,' +
'position_status=position_status,' +
'verification_status=verification_status,' +
'sort_order=sort_order',
'stall_id IN (' +
makeInClause(
stallIds
) +
') AND year=' +
compareYear,
100
)
] ).then(
function ( firstResults ) {
const stalls =
firstResults[ 0 ];
const placements =
firstResults[ 1 ];
/*
* 同じstallに複数Placementがある場合
* sort_orderの小さいものを
* 今回の比較対象にする
*
* 後でfestival_idまで比較条件に
* 含める設計へ拡張可能
*/
const placementByStall = {};
placements
.slice()
.sort(
function ( a, b ) {
return (
Number(
a.sort_order || 0
) -
Number(
b.sort_order || 0
)
);
}
)
.forEach(
function (
placement
) {
const id =
String(
placement.stall_id
);
if (
!placementByStall[
id
]
) {
placementByStall[
id
] =
placement;
}
}
);
const chosenPlacements =
stallIds
.map(
function ( id ) {
return (
placementByStall[
id
] ||
null
);
}
)
.filter(
Boolean
);
const festivalIds =
uniqueIds(
chosenPlacements.map(
function ( row ) {
return row.festival_id;
}
)
);
const venueIds =
uniqueIds(
chosenPlacements.map(
function ( row ) {
return row.venue_id;
}
)
);
const placementIds =
uniqueIds(
chosenPlacements.map(
function ( row ) {
return row.placement_id;
}
)
);
/*
* STEP 2
*
* Festivals
* Venues
* Offerings
*/
return Promise.all( [
festivalIds.length
? cargoQuery(
'Festivals',
'festival_id=festival_id,' +
'name=festival_name,' +
'_pageName=page_name',
'festival_id IN (' +
makeInClause(
festivalIds
) +
')',
100
)
: Promise.resolve(
[]
),
venueIds.length
? cargoQuery(
'Venues',
'venue_id=venue_id,' +
'name=venue_name,' +
'area_id=area_id,' +
'_pageName=page_name',
'venue_id IN (' +
makeInClause(
venueIds
) +
')',
100
)
: Promise.resolve(
[]
),
placementIds.length
? cargoQuery(
'FestivalStallMenuOfferings',
'offering_id=offering_id,' +
'placement_id=placement_id,' +
'menu_item_id=menu_item_id,' +
'price=price,' +
'serving_quantity=serving_quantity,' +
'serving_unit=serving_unit,' +
'serving_note=serving_note,' +
'availability=availability,' +
'verification_status=verification_status,' +
'sort_order=sort_order',
'placement_id IN (' +
makeInClause(
placementIds
) +
')',
100
)
: Promise.resolve(
[]
)
] ).then(
function (
secondResults
) {
return {
stalls: stalls,
placementByStall:
placementByStall,
festivals:
secondResults[ 0 ],
venues:
secondResults[ 1 ],
offerings:
secondResults[ 2 ]
};
}
);
}
).then(
function ( data ) {
const areaIds =
uniqueIds(
data.venues.map(
function ( row ) {
return row.area_id;
}
)
);
const menuItemIds =
uniqueIds(
data.offerings.map(
function ( row ) {
return row.menu_item_id;
}
)
);
/*
* STEP 3
*
* Areas
* StallMenuItems
*/
return Promise.all( [
areaIds.length
? cargoQuery(
'Areas',
'area_id=area_id,' +
'name=area_name,' +
'_pageName=page_name',
'area_id IN (' +
makeInClause(
areaIds
) +
')',
100
)
: Promise.resolve(
[]
),
menuItemIds.length
? cargoQuery(
'StallMenuItems',
'menu_item_id=menu_item_id,' +
'stall_id=stall_id,' +
'name=menu_name,' +
'item_category=item_category,' +
'sort_order=sort_order',
'menu_item_id IN (' +
makeInClause(
menuItemIds
) +
')',
100
)
: Promise.resolve(
[]
)
] ).then(
function (
thirdResults
) {
data.areas =
thirdResults[ 0 ];
data.menuItems =
thirdResults[ 1 ];
return data;
}
);
}
).then(
function ( data ) {
/* ============================
* Map化
* ============================ */
const stallMap =
mapBy(
data.stalls,
'stall_id'
);
const festivalMap =
mapBy(
data.festivals,
'festival_id'
);
const venueMap =
mapBy(
data.venues,
'venue_id'
);
const areaMap =
mapBy(
data.areas,
'area_id'
);
const menuMap =
mapBy(
data.menuItems,
'menu_item_id'
);
/* ============================
* OfferingをPlacementごとに
* ============================ */
const offeringsByPlacement =
{};
data.offerings
.slice()
.sort(
function ( a, b ) {
return (
Number(
a.sort_order || 0
) -
Number(
b.sort_order || 0
)
);
}
)
.forEach(
function (
offering
) {
const placementId =
String(
offering
.placement_id
);
if (
!offeringsByPlacement[
placementId
]
) {
offeringsByPlacement[
placementId
] = [];
}
offeringsByPlacement[
placementId
].push(
offering
);
}
);
/* ============================
* 比較データを
* localStorage順に生成
* ============================ */
const compareData =
stallIds.map(
function ( stallId ) {
const stall =
stallMap[
stallId
] || null;
const placement =
data
.placementByStall[
stallId
] || null;
let festival = null;
let venue = null;
let area = null;
let menus = [];
if ( placement ) {
festival =
festivalMap[
String(
placement
.festival_id
)
] || null;
venue =
venueMap[
String(
placement
.venue_id
)
] || null;
if (
venue &&
venue.area_id
) {
area =
areaMap[
String(
venue
.area_id
)
] || null;
}
const offerings =
offeringsByPlacement[
String(
placement
.placement_id
)
] || [];
menus =
offerings.map(
function (
offering
) {
const menu =
menuMap[
String(
offering
.menu_item_id
)
] || {};
return {
menuName:
menu.menu_name ||
'商品',
category:
menu.item_category ||
'',
price:
cleanNumber(
offering.price
),
servingQuantity:
cleanNumber(
offering
.serving_quantity
),
servingUnit:
offering
.serving_unit ||
'',
servingNote:
offering
.serving_note ||
'',
unitPrice:
getUnitPrice(
offering
),
availability:
formatAvailability(
offering
.availability
),
verification:
formatVerification(
offering
.verification_status
)
};
}
);
}
return {
stallId: stallId,
stall: stall,
placement:
placement,
festival:
festival,
venue:
venue,
area:
area,
menus:
menus
};
}
);
renderComparison(
compareData
);
}
).catch(
function ( error ) {
console.error(
'屋台比較データ取得エラー:',
error
);
renderMessage(
'比較データの取得中にエラーが発生しました。'
);
}
);
} );