編集の要約なし
編集の要約なし
1,382行目: 1,382行目:
/* ========================================
/* ========================================
  * 屋台比較ページ
  * 屋台比較ページ
  *
  * placement_id 正式版
* localStorage:
* matsuriWikiCompareStalls
*
* 2~4店舗をCargoから取得して
* 横並び比較表を自動生成
  * ======================================== */
  * ======================================== */


1,397行目: 1,392行目:


     'use strict';
     'use strict';


     const compareRoot =
     const compareRoot =
1,402行目: 1,398行目:
             'stall-compare-page'
             'stall-compare-page'
         );
         );


     /*
     /*
     * 屋台比較ページ以外では
     * 屋台比較ページ以外では終了
    * 何もしない
     */
     */
     if ( !compareRoot ) {
     if ( !compareRoot ) {
1,413行目: 1,409行目:


     const STORAGE_KEY =
     const STORAGE_KEY =
         'matsuriWikiCompareStalls';
         'matsuriWikiComparePlacements';


     const MIN_COMPARE = 2;
     const MIN_COMPARE = 2;
1,423行目: 1,419行目:


     /* =====================================
     /* =====================================
     * 比較年
     * localStorage
    *
    * ?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() {
     function getPlacementIds() {


         const raw =
         const raw =
1,473行目: 1,428行目:
                 STORAGE_KEY
                 STORAGE_KEY
             );
             );


         if ( !raw ) {
         if ( !raw ) {
             return [];
             return [];
         }
         }


         try {
         try {


             const ids =
             const ids =
                 JSON.parse( raw );
                 JSON.parse(
                    raw
                );


             if ( !Array.isArray( ids ) ) {
 
             if (
                !Array.isArray(
                    ids
                )
            ) {
                 return [];
                 return [];
             }
             }


             return [ ...new Set(
             return [ ...new Set(
1,501行目: 1,466行目:
                 MAX_COMPARE
                 MAX_COMPARE
             );
             );


         } catch ( e ) {
         } catch ( e ) {
1,512行目: 1,478行目:


     /* =====================================
     /* =====================================
     * Cargo API共通処理
     * Cargo
     * ===================================== */
     * ===================================== */


1,535行目: 1,501行目:


         };
         };


         if ( where ) {
         if ( where ) {
1,554行目: 1,521行目:
                     return [];
                     return [];
                 }
                 }


                 return data.cargoquery.map(
                 return data.cargoquery.map(
1,571行目: 1,539行目:
     }
     }


    /* =====================================
    * ID配列をCargo IN句にする
    * ===================================== */


     function makeInClause( ids ) {
     function makeInClause( ids ) {


         return ids
         return ids
            .map( String )
             .filter(
             .filter(
                 function ( id ) {
                 function ( id ) {
                     return /^\d+$/.test(
                     return /^\d+$/.test(
                         String( id )
                         id
                     );
                     );
                 }
                 }
1,590行目: 1,555行目:
     }
     }


    /* =====================================
    * 重複削除
    * ===================================== */


     function uniqueIds( values ) {
     function uniqueIds( values ) {
1,602行目: 1,563行目:
                     .map( String )
                     .map( String )
                     .filter(
                     .filter(
                         function ( value ) {
                         function ( id ) {


                             return (
                             return (
                                 value &&
                                 id &&
                                 /^\d+$/.test(
                                 /^\d+$/.test(
                                     value
                                     id
                                 )
                                 )
                             );
                             );
1,618行目: 1,579行目:
     }
     }


    /* =====================================
    * Object Map作成
    * ===================================== */


     function mapBy(
     function mapBy(
1,629行目: 1,586行目:


         const result = {};
         const result = {};


         rows.forEach(
         rows.forEach(
1,639行目: 1,597行目:
                     return;
                     return;
                 }
                 }


                 result[
                 result[
1,648行目: 1,607行目:
             }
             }
         );
         );


         return result;
         return result;
1,655行目: 1,615行目:


     /* =====================================
     /* =====================================
     * 表示用関数
     * 表示ヘルパー
     * ===================================== */
     * ===================================== */


     function textOrDash( value ) {
     function textOrDash(
        value
    ) {


         if (
         if (
1,668行目: 1,630行目:
         }
         }


         return String( value );
         return String(
            value
        );
 
     }
     }




     function cleanNumber( value ) {
     function cleanNumber(
        value
    ) {


         const number =
         const number =
             Number( value );
             Number(
                value
            );
 


         if (
         if (
1,684行目: 1,654行目:
             return '';
             return '';
         }
         }


         if (
         if (
1,690行目: 1,661行目:
             )
             )
         ) {
         ) {
             return String(
             return String(
                 number
                 number
             );
             );
         }
         }


         return String(
         return String(
1,711行目: 1,685行目:
             return '―';
             return '―';
         }
         }


         const open =
         const open =
1,717行目: 1,692行目:
         const close =
         const close =
             placement.closing_time || '';
             placement.closing_time || '';


         if (
         if (
1,722行目: 1,698行目:
             close
             close
         ) {
         ) {
             return (
             return (
                 open +
                 open +
1,727行目: 1,704行目:
                 close
                 close
             );
             );
         }
         }


         if ( open ) {
         if ( open ) {
             return open + '~';
 
             return (
                open +
                '~'
            );
 
         }
         }


         if ( close ) {
         if ( close ) {
             return '~' + close;
 
             return (
                '~' +
                close
            );
 
         }
         }


         if (
         if (
             placement.hours_note
             placement.hours_note
         ) {
         ) {
             return placement.hours_note;
             return placement.hours_note;
         }
         }


         return '未確認';
         return '未確認';
1,815行目: 1,809行目:


     /* =====================================
     /* =====================================
     * 1単位あたり価格
     * 単位価格
     * ===================================== */
     * ===================================== */


1,821行目: 1,815行目:
         offering
         offering
     ) {
     ) {
        if ( !offering ) {
            return '―';
        }


         const price =
         const price =
1,835行目: 1,825行目:
                 offering.serving_quantity
                 offering.serving_quantity
             );
             );


         if (
         if (
1,845行目: 1,836行目:
             quantity <= 0
             quantity <= 0
         ) {
         ) {
             return '―';
             return '―';
         }
         }


         const unitPrice =
         const unitPrice =
1,853行目: 1,847行目:
                     price /
                     price /
                     quantity
                     quantity
                 ) * 100
                 ) *
             ) / 100;
                100
             ) /
            100;
 


         const unit =
         const unit =
             offering.serving_unit ||
             offering.serving_unit ||
             '単位';
             '単位';


         return (
         return (
1,870行目: 1,868行目:


     /* =====================================
     /* =====================================
     * DOM helper
     * DOM
     * ===================================== */
     * ===================================== */


1,892行目: 1,890行目:


     /* =====================================
     /* =====================================
     * Menu表示
     * メニュー
     * ===================================== */
     * ===================================== */


     function createMenuList(
     function createMenuList(
         menuData
         menus
     ) {
     ) {


1,909行目: 1,907行目:


         if (
         if (
             !menuData ||
             !menus ||
             menuData.length === 0
             menus.length === 0
         ) {
         ) {


1,921行目: 1,919行目:




         menuData.forEach(
         menus.forEach(
             function ( item ) {
             function ( item ) {


1,953行目: 1,951行目:
                 price.textContent =
                 price.textContent =
                     item.price
                     item.price
                         ? item.price + '円'
                         ? item.price +
                          '円'
                         : '価格未確認';
                         : '価格未確認';


1,961行目: 1,960行目:
                         'div'
                         'div'
                     );
                     );


                 if (
                 if (
1,982行目: 1,982行目:




                 const perUnit =
                 const unit =
                     document.createElement(
                     document.createElement(
                         'div'
                         'div'
                     );
                     );


                 perUnit.textContent =
                 unit.textContent =
                     '1単位あたり:' +
                     '1単位あたり:' +
                     item.unitPrice;
                     item.unitPrice;
2,015行目: 2,015行目:


                 menu.appendChild(
                 menu.appendChild(
                     perUnit
                     unit
                 );
                 );


2,037行目: 2,037行目:


     /* =====================================
     /* =====================================
     * 比較表生成
     * 比較表
     * ===================================== */
     * ===================================== */


2,044行目: 2,044行目:
     ) {
     ) {


         compareRoot.innerHTML = '';
         compareRoot.innerHTML =
            '';




        /*
        * 年表示
        */
         const heading =
         const heading =
             document.createElement(
             document.createElement(
2,056行目: 2,054行目:


         heading.textContent =
         heading.textContent =
            compareYear +
             '屋台比較';
             '年の屋台比較';
 


         compareRoot.appendChild(
         compareRoot.appendChild(
2,064行目: 2,062行目:




        /*
        * 横スクロール用
        */
         const wrapper =
         const wrapper =
             document.createElement(
             document.createElement(
2,085行目: 2,080行目:




         /* ==============================
         /* ------------------------------
         * ヘッダー
         * thead
         * ============================== */
         * ------------------------------ */


         const thead =
         const thead =
2,098行目: 2,093行目:
                 'tr'
                 'tr'
             );
             );


         headerRow.appendChild(
         headerRow.appendChild(
2,128行目: 2,124行目:
                     link.href =
                     link.href =
                         mw.util.getUrl(
                         mw.util.getUrl(
                             data.stall.page_name
                             data.stall
                                .page_name
                         );
                         );


                     link.textContent =
                     link.textContent =
                         data.stall.stall_name ||
                         data.stall
                            .stall_name ||
                         '屋台';
                         '屋台';


                     th.appendChild(
                     th.appendChild(
2,171行目: 2,170行目:
             );
             );


        /* ==============================
        * 普通の文字列行
        * ============================== */


         function addRow(
         function addRow(
2,185行目: 2,180行目:
                     'tr'
                     'tr'
                 );
                 );


             const labelCell =
             const labelCell =
2,194行目: 2,190行目:
             labelCell.scope =
             labelCell.scope =
                 'row';
                 'row';


             tr.appendChild(
             tr.appendChild(
2,203行目: 2,200行目:
                 function ( data ) {
                 function ( data ) {


                     const td =
                     tr.appendChild(
                         createTextCell(
                         createTextCell(
                             'td',
                             'td',
2,211行目: 2,208行目:
                                 )
                                 )
                             )
                             )
                         );
                         )
 
                    tr.appendChild(
                        td
                     );
                     );


2,227行目: 2,221行目:
         }
         }


        /* =================================
        * Placement情報
        * ================================= */


         addRow(
         addRow(
             '祭り',
            '開催年',
            function ( data ) {
 
                return data.placement
                    ? data.placement.year +
                      '年'
                    : '―';
 
            }
        );
 
 
        addRow(
             '祭り',
             function ( data ) {
             function ( data ) {


                 return data.festival
                 return data.festival
                     ? data.festival.festival_name
                     ? data.festival
                        .festival_name
                     : '―';
                     : '―';


2,245行目: 2,257行目:


                 return data.venue
                 return data.venue
                     ? data.venue.venue_name
                     ? data.venue
                        .venue_name
                     : '―';
                     : '―';


2,257行目: 2,270行目:


                 return data.area
                 return data.area
                     ? data.area.area_name
                     ? data.area
                        .area_name
                     : '―';
                     : '―';


2,269行目: 2,283行目:


                 return data.stall
                 return data.stall
                     ? data.stall.category
                     ? data.stall
                        .category
                     : '―';
                     : '―';


2,281行目: 2,296行目:


                 return data.placement
                 return data.placement
                     ? data.placement.location_note
                     ? data.placement
                        .location_note
                     : '―';
                     : '―';


2,330行目: 2,346行目:




         /* ==============================
         /* =================================
         * Menu行
         * メニュー
         * ============================== */
         * ================================= */


         const menuRow =
         const menuRow =
2,338行目: 2,354行目:
                 'tr'
                 'tr'
             );
             );


         const menuLabel =
         const menuLabel =
2,347行目: 2,364行目:
         menuLabel.scope =
         menuLabel.scope =
             'row';
             'row';


         menuRow.appendChild(
         menuRow.appendChild(
2,360行目: 2,378行目:
                         'td'
                         'td'
                     );
                     );


                 td.appendChild(
                 td.appendChild(
2,366行目: 2,385行目:
                     )
                     )
                 );
                 );


                 menuRow.appendChild(
                 menuRow.appendChild(
2,393行目: 2,413行目:




        /*
        * 注意書き
        */
         const note =
         const note =
             document.createElement(
             document.createElement(
2,405行目: 2,422行目:


         note.textContent =
         note.textContent =
             '価格・営業時間・出店位置は開催年によって変わる場合があります。確認状態もあわせて参照してください。';
             'この比較は出店単位(Placement)です。祭り・開催年・会場ごとの価格、営業時間、出店位置を比較しています。';
 


         compareRoot.appendChild(
         compareRoot.appendChild(
2,413行目: 2,431行目:
     }
     }


    /* =====================================
    * エラー表示
    * ===================================== */


     function renderMessage(
     function renderMessage(
2,422行目: 2,436行目:
     ) {
     ) {


         compareRoot.innerHTML = '';
         compareRoot.innerHTML =
            '';
 


         const p =
         const p =
2,434行目: 2,450行目:
         p.textContent =
         p.textContent =
             message;
             message;


         compareRoot.appendChild(
         compareRoot.appendChild(
2,443行目: 2,460行目:


     /* =====================================
     /* =====================================
     * データ取得開始
     * データ取得
     * ===================================== */
     * ===================================== */


     const stallIds =
     const placementIds =
         getCompareStallIds();
         getPlacementIds();




     if (
     if (
         stallIds.length <
         placementIds.length <
         MIN_COMPARE
         MIN_COMPARE
     ) {
     ) {


         renderMessage(
         renderMessage(
             '比較する屋台を2店舗以上選択してください。'
             '比較する出店を2件以上選択してください。'
         );
         );


2,466行目: 2,483行目:
     /*
     /*
     * STEP 1
     * STEP 1
     *
     * Placementを直接取得
    * Stalls
    * FestivalStallPlacements
     */
     */
     Promise.all( [
     cargoQuery(


         cargoQuery(
         'FestivalStallPlacements',


            'Stalls',
        'placement_id=placement_id,' +
        'stall_id=stall_id,' +
        'festival_id=festival_id,' +
        'venue_id=venue_id,' +
        'year=year,' +
        'latitude=latitude,' +
        'longitude=longitude,' +
        'location_note=location_note,' +
        'opening_time=opening_time,' +
        'closing_time=closing_time,' +
        'hours_note=hours_note,' +
        'position_status=position_status,' +
        'verification_status=verification_status',


            'stall_id=stall_id,' +
        'placement_id IN (' +
             'name=stall_name,' +
        makeInClause(
            'category=category,' +
             placementIds
            '_pageName=page_name',
        ) +
        ')',


            'stall_id IN (' +
        100
            makeInClause(
                stallIds
            ) +
            ')',


            MAX_COMPARE
    ).then(
        function ( placements ) {


        ),
            const stallIds =
 
                uniqueIds(
 
                    placements.map(
        cargoQuery(
                        function ( row ) {
                            return row.stall_id;
                        }
                    )
                );


            'FestivalStallPlacements',


             'placement_id=placement_id,' +
             const festivalIds =
            'stall_id=stall_id,' +
                uniqueIds(
            'festival_id=festival_id,' +
                    placements.map(
            'venue_id=venue_id,' +
                        function ( row ) {
            'year=year,' +
                            return row.festival_id;
            '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
             const venueIds =
                uniqueIds(
                    placements.map(
                        function ( row ) {
                            return row.venue_id;
                        }
                    )
                );


        )


    ] ).then(
            /*
        function ( firstResults ) {
            * STEP 2
            */
            return Promise.all( [


            const stalls =
                stallIds.length
                firstResults[ 0 ];
                    ? cargoQuery(


            const placements =
                        'Stalls',
                firstResults[ 1 ];


                        'stall_id=stall_id,' +
                        'name=stall_name,' +
                        'category=category,' +
                        '_pageName=page_name',


            /*
                        'stall_id IN (' +
            * 同じstallに複数Placementがある場合
                        makeInClause(
            * sort_orderの小さいものを
                            stallIds
            * 今回の比較対象にする
                        ) +
            *
                        ')',
            * 後でfestival_idまで比較条件に
            * 含める設計へ拡張可能
            */
            const placementByStall = {};


            placements
                        100
                .slice()
                .sort(
                    function ( a, b ) {


                        return (
                    )
                            Number(
                    : Promise.resolve(
                                a.sort_order || 0
                        []
                            ) -
                    ),
                            Number(
                                b.sort_order || 0
                            )
                        );


                    }
                )
                .forEach(
                    function (
                        placement
                    ) {


                        const id =
                festivalIds.length
                            String(
                    ? cargoQuery(
                                placement.stall_id
                            );


                         if (
                         'Festivals',
                            !placementByStall[
                                id
                            ]
                        ) {


                            placementByStall[
                        'festival_id=festival_id,' +
                                id
                        'name=festival_name,' +
                            ] =
                        '_pageName=page_name',
                                placement;


                         }
                         'festival_id IN (' +
                        makeInClause(
                            festivalIds
                        ) +
                        ')',


                    }
                        100
                );


                    )
                    : Promise.resolve(
                        []
                    ),


            const chosenPlacements =
                stallIds
                    .map(
                        function ( id ) {


                            return (
                venueIds.length
                                placementByStall[
                    ? cargoQuery(
                                    id
                                ] ||
                                null
                            );


                         }
                         'Venues',
                    )
                    .filter(
                        Boolean
                    );


                        'venue_id=venue_id,' +
                        'name=venue_name,' +
                        'area_id=area_id,' +
                        '_pageName=page_name',


            const festivalIds =
                        'venue_id IN (' +
                uniqueIds(
                         makeInClause(
                    chosenPlacements.map(
                             venueIds
                         function ( row ) {
                         ) +
                             return row.festival_id;
                        ')',
                         }
                    )
                );


                        100


            const venueIds =
                uniqueIds(
                    chosenPlacements.map(
                        function ( row ) {
                            return row.venue_id;
                        }
                     )
                     )
                );
                    : Promise.resolve(
                        []
                    ),




            const placementIds =
                 cargoQuery(
                 uniqueIds(
                    chosenPlacements.map(
                        function ( row ) {
                            return row.placement_id;
                        }
                    )
                );


                    'FestivalStallMenuOfferings',


            /*
                    'offering_id=offering_id,' +
            * STEP 2
                    'placement_id=placement_id,' +
            *
                    'menu_item_id=menu_item_id,' +
            * Festivals
                    'price=price,' +
            * Venues
                    'serving_quantity=serving_quantity,' +
            * Offerings
                    'serving_unit=serving_unit,' +
            */
                    'serving_note=serving_note,' +
            return Promise.all( [
                    'availability=availability,' +
                    'verification_status=verification_status,' +
                    'sort_order=sort_order',


                festivalIds.length
                    'placement_id IN (' +
                     ? cargoQuery(
                     makeInClause(
                        placementIds
                    ) +
                    ')',


                        'Festivals',
                    100


                        'festival_id=festival_id,' +
                )
                        'name=festival_name,' +
                        '_pageName=page_name',


                        'festival_id IN (' +
            ] ).then(
                        makeInClause(
                function ( results ) {
                            festivalIds
 
                        ) +
                    return {
                        ')',


                         100
                         placements:
                            placements,


                    )
                        stalls:
                    : Promise.resolve(
                            results[ 0 ],
                        []
                    ),


                        festivals:
                            results[ 1 ],


                venueIds.length
                        venues:
                    ? cargoQuery(
                            results[ 2 ],


                         'Venues',
                         offerings:
                            results[ 3 ]


                        'venue_id=venue_id,' +
                    };
                        'name=venue_name,' +
                        'area_id=area_id,' +
                        '_pageName=page_name',


                        'venue_id IN (' +
                }
                        makeInClause(
            );
                            venueIds
                        ) +
                        ')',


                        100
        }
    ).then(
        function ( data ) {


            const areaIds =
                uniqueIds(
                    data.venues.map(
                        function ( row ) {
                            return row.area_id;
                        }
                     )
                     )
                    : Promise.resolve(
                );
                        []
                    ),




                 placementIds.length
            const menuItemIds =
                    ? cargoQuery(
                 uniqueIds(
 
                    data.offerings.map(
                         'FestivalStallMenuOfferings',
                        function ( row ) {
 
                            return row.menu_item_id;
                        '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,' +
            * STEP 3
                         'availability=availability,' +
            */
                         'verification_status=verification_status,' +
            return Promise.all( [
                         'sort_order=sort_order',
 
                areaIds.length
                    ? cargoQuery(
 
                         'Areas',
 
                         'area_id=area_id,' +
                         'name=area_name,' +
                         '_pageName=page_name',


                         'placement_id IN (' +
                         'area_id IN (' +
                         makeInClause(
                         makeInClause(
                             placementIds
                             areaIds
                         ) +
                         ) +
                         ')',
                         ')',
2,715行目: 2,719行目:
                     : Promise.resolve(
                     : Promise.resolve(
                         []
                         []
                     )
                     ),


            ] ).then(
                function (
                    secondResults
                ) {


                     return {
                menuItemIds.length
                     ? cargoQuery(


                         stalls: stalls,
                         'StallMenuItems',


                         placementByStall:
                         'menu_item_id=menu_item_id,' +
                            placementByStall,
                        'stall_id=stall_id,' +
                        'name=menu_name,' +
                        'item_category=item_category,' +
                        'sort_order=sort_order',


                         festivals:
                         'menu_item_id IN (' +
                             secondResults[ 0 ],
                        makeInClause(
                             menuItemIds
                        ) +
                        ')',


                         venues:
                         100
                            secondResults[ 1 ],


                        offerings:
                    )
                            secondResults[ 2 ]
                    : Promise.resolve(
                        []
                    )


                    };
            ] ).then(
                function ( results ) {


                 }
                    data.areas =
                        results[ 0 ];
 
                    data.menuItems =
                        results[ 1 ];
 
                    return data;
 
                 }
             );
             );


2,747行目: 2,764行目:
         function ( data ) {
         function ( data ) {


             const areaIds =
             const placementMap =
                 uniqueIds(
                 mapBy(
                     data.venues.map(
                     data.placements,
                        function ( row ) {
                     'placement_id'
                            return row.area_id;
                        }
                     )
                 );
                 );




             const menuItemIds =
             const stallMap =
                 uniqueIds(
                 mapBy(
                     data.offerings.map(
                     data.stalls,
                        function ( row ) {
                     'stall_id'
                            return row.menu_item_id;
                        }
                     )
                 );
                 );




             /*
             const festivalMap =
            * STEP 3
                mapBy(
            *
                    data.festivals,
            * Areas
                    'festival_id'
            * StallMenuItems
                );
            */
            return Promise.all( [


                areaIds.length
                    ? cargoQuery(


                        'Areas',
            const venueMap =
                mapBy(
                    data.venues,
                    'venue_id'
                );


                        'area_id=area_id,' +
                        'name=area_name,' +
                        '_pageName=page_name',


                        'area_id IN (' +
            const areaMap =
                        makeInClause(
                mapBy(
                            areaIds
                    data.areas,
                        ) +
                    'area_id'
                        ')',
                );


                        100


                    )
            const menuMap =
                     : Promise.resolve(
                mapBy(
                        []
                     data.menuItems,
                    ),
                    'menu_item_id'
                );




                 menuItemIds.length
            /*
                    ? cargoQuery(
            * Offering
            * placement単位
            */
            const offeringsByPlacement =
                 {};


                        'StallMenuItems',


                        'menu_item_id=menu_item_id,' +
            data.offerings
                        'stall_id=stall_id,' +
                .slice()
                        'name=menu_name,' +
                .sort(
                        'item_category=item_category,' +
                    function ( a, b ) {
                        'sort_order=sort_order',


                         'menu_item_id IN (' +
                         return (
                        makeInClause(
                            Number(
                             menuItemIds
                                a.sort_order || 0
                        ) +
                            ) -
                         ')',
                             Number(
                                b.sort_order || 0
                            )
                         );


                        100
                    }
                )
                .forEach(
                    function ( offering ) {


                    )
                        const placementId =
                    : Promise.resolve(
                            String(
                        []
                                offering
                    )
                                    .placement_id
                            );


            ] ).then(
                function (
                    thirdResults
                ) {


                    data.areas =
                        if (
                        thirdResults[ 0 ];
                            !offeringsByPlacement[
                                placementId
                            ]
                        ) {


                    data.menuItems =
                            offeringsByPlacement[
                        thirdResults[ 1 ];
                                placementId
                            ] = [];


                    return data;
                        }


                }
            );


        }
                        offeringsByPlacement[
    ).then(
                            placementId
        function ( data ) {
                        ].push(
                            offering
                        );


            /* ============================
                     }
            * Map化
            * ============================ */
 
            const stallMap =
                mapBy(
                     data.stalls,
                    'stall_id'
                 );
                 );


            const festivalMap =
                mapBy(
                    data.festivals,
                    'festival_id'
                );


             const venueMap =
            /*
                 mapBy(
            * localStorage順を維持
                     data.venues,
            */
                     'venue_id'
             const compareData =
                );
                 placementIds.map(
                     function (
                        placementId
                     ) {


            const areaMap =
                        const placement =
                mapBy(
                            placementMap[
                    data.areas,
                                placementId
                    'area_id'
                            ] || null;
                );
 
 
                        if ( !placement ) {


            const menuMap =
                            return {
                mapBy(
                    data.menuItems,
                    'menu_item_id'
                );


                                placementId:
                                    placementId,


            /* ============================
                                placement:
            * OfferingをPlacementごとに
                                    null,
            * ============================ */


            const offeringsByPlacement =
                                stall:
                {};
                                    null,


                                festival:
                                    null,


            data.offerings
                                venue:
                .slice()
                                    null,
                .sort(
                    function ( a, b ) {


                        return (
                                 area:
                            Number(
                                    null,
                                 a.sort_order || 0
                            ) -
                            Number(
                                b.sort_order || 0
                            )
                        );


                    }
                                menus:
                )
                                    []
                .forEach(
                    function (
                        offering
                    ) {


                        const placementId =
                             };
                             String(
                                offering
                                    .placement_id
                            );


                         if (
                         }
                            !offeringsByPlacement[
                                placementId
                            ]
                        ) {


                            offeringsByPlacement[
                                placementId
                            ] = [];


                         }
                         const stall =
                            stallMap[
                                String(
                                    placement.stall_id
                                )
                            ] || null;


                        offeringsByPlacement[
                            placementId
                        ].push(
                            offering
                        );


                    }
                        const festival =
                );
                            festivalMap[
                                String(
                                    placement.festival_id
                                )
                            ] || null;




            /* ============================
                        const venue =
            * 比較データを
                            venueMap[
            * localStorage順に生成
                                String(
            * ============================ */
                                    placement.venue_id
 
                                 )
            const compareData =
                stallIds.map(
                    function ( stallId ) {
 
                        const stall =
                            stallMap[
                                 stallId
                             ] || null;
                             ] || null;




                        const placement =
                            data
                                .placementByStall[
                                    stallId
                                ] || null;
                        let festival = null;
                        let venue = null;
                         let area = null;
                         let area = null;


                        let menus = [];


                        if (
                            venue &&
                            venue.area_id
                        ) {


                        if ( placement ) {
                             area =
 
                                 areaMap[
                             festival =
                                 festivalMap[
                                     String(
                                     String(
                                         placement
                                         venue.area_id
                                            .festival_id
                                     )
                                     )
                                 ] || null;
                                 ] || null;


                        }


                            venue =
                                venueMap[
                                    String(
                                        placement
                                            .venue_id
                                    )
                                ] || null;


                        const offerings =
                            offeringsByPlacement[
                                placementId
                            ] || [];


                            if (
                                venue &&
                                venue.area_id
                            ) {


                                area =
                        const menus =
                                    areaMap[
                            offerings.map(
                                        String(
                                function (
                                            venue
                                    offering
                                                .area_id
                                ) {
                                        )
                                    ] || null;


                            }
                                    const menu =
                                        menuMap[
                                            String(
                                                offering
                                                    .menu_item_id
                                            )
                                        ] || {};




                            const offerings =
                                     return {
                                offeringsByPlacement[
                                     String(
                                        placement
                                            .placement_id
                                    )
                                ] || [];


                                        menuName:
                                            menu.menu_name ||
                                            '商品',


                            menus =
                                        category:
                                offerings.map(
                                            menu.item_category ||
                                    function (
                                            '',
                                        offering
                                    ) {


                                         const menu =
                                         price:
                                             menuMap[
                                             cleanNumber(
                                                 String(
                                                 offering.price
                                                    offering
                                            ),
                                                        .menu_item_id
                                                )
                                            ] || {};


                                        servingQuantity:
                                            cleanNumber(
                                                offering
                                                    .serving_quantity
                                            ),


                                         return {
                                         servingUnit:
                                            offering
                                                .serving_unit ||
                                            '',


                                             menuName:
                                        servingNote:
                                                 menu.menu_name ||
                                             offering
                                                '商品',
                                                 .serving_note ||
                                            '',


                                            category:
                                        unitPrice:
                                                menu.item_category ||
                                             getUnitPrice(
                                                '',
                                                 offering
 
                                             ),
                                             price:
                                                 cleanNumber(
                                                    offering.price
                                                ),
 
                                             servingQuantity:
                                                cleanNumber(
                                                    offering
                                                        .serving_quantity
                                                ),


                                             servingUnit:
                                        availability:
                                             formatAvailability(
                                                 offering
                                                 offering
                                                     .serving_unit ||
                                                     .availability
                                                '',
                                            ),


                                             servingNote:
                                        verification:
                                             formatVerification(
                                                 offering
                                                 offering
                                                     .serving_note ||
                                                     .verification_status
                                                '',
                                            )


                                            unitPrice:
                                    };
                                                getUnitPrice(
                                                    offering
                                                ),


                                            availability:
                                }
                                                formatAvailability(
                            );
                                                    offering
                                                        .availability
                                                ),
 
                                            verification:
                                                formatVerification(
                                                    offering
                                                        .verification_status
                                                )
 
                                        };
 
                                    }
                                );
 
                        }




                         return {
                         return {


                             stallId: stallId,
                             placementId:
 
                                placementId,
                            stall: stall,


                             placement:
                             placement:
                                 placement,
                                 placement,
                            stall:
                                stall,


                             festival:
                             festival:
3,115行目: 3,063行目:


             console.error(
             console.error(
                 '屋台比較データ取得エラー:',
                 'Placement比較データ取得エラー:',
                 error
                 error
             );
             );


             renderMessage(
             renderMessage(

2026年8月12日 (水) 01:38時点における版

/* ========================================
 * 屋台比較
 * placement_id 正式版
 *
 * 最大4出店
 *
 * localStorage:
 * matsuriWikiComparePlacements
 *
 * 1 placement =
 * 1 festival + 1 year + 1 venue + 1 stall
 * ======================================== */

mw.loader.using( [
    'mediawiki.storage',
    'mediawiki.api',
    'mediawiki.util'
] ).then( function () {

    'use strict';

    const STORAGE_KEY =
        'matsuriWikiComparePlacements';

    const MAX_COMPARE = 4;

    const api =
        new mw.Api();

    /*
     * ページ表示中だけ使うキャッシュ
     */
    const placementInfoCache = {};


    /* =====================================
     * localStorage
     * ===================================== */

    function getComparePlacements() {

        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 [];

        }

    }


    function saveComparePlacements( ids ) {

        mw.storage.set(
            STORAGE_KEY,
            JSON.stringify( ids )
        );

    }


    /* =====================================
     * Cargo共通処理
     * ===================================== */

    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;
                    }
                );

            }
        );

    }


    function makeInClause( ids ) {

        return ids
            .map( String )
            .filter(
                function ( id ) {
                    return /^\d+$/.test( id );
                }
            )
            .join( ',' );

    }


    function uniqueIds( values ) {

        return [
            ...new Set(
                values
                    .map( String )
                    .filter(
                        function ( id ) {
                            return /^\d+$/.test( id );
                        }
                    )
            )
        ];

    }


    function mapBy( rows, key ) {

        const result = {};

        rows.forEach(
            function ( row ) {

                if (
                    row[ key ] === undefined ||
                    row[ key ] === null
                ) {
                    return;
                }

                result[
                    String(
                        row[ key ]
                    )
                ] = row;

            }
        );

        return result;

    }


    /* =====================================
     * Placement情報取得
     * ===================================== */

    function fetchPlacementInfo( ids ) {

        if ( ids.length === 0 ) {
            return Promise.resolve( {} );
        }

        const result = {};
        const missingIds = [];

        ids.forEach(
            function ( id ) {

                if (
                    placementInfoCache[ id ]
                ) {

                    result[ id ] =
                        placementInfoCache[ id ];

                } else {

                    missingIds.push( id );

                }

            }
        );


        if ( missingIds.length === 0 ) {

            return Promise.resolve(
                result
            );

        }


        return cargoQuery(

            'FestivalStallPlacements',

            'placement_id=placement_id,' +
            'stall_id=stall_id,' +
            'festival_id=festival_id,' +
            'venue_id=venue_id,' +
            'year=year,' +
            'location_note=location_note',

            'placement_id IN (' +
            makeInClause(
                missingIds
            ) +
            ')',

            100

        ).then(
            function ( placements ) {

                const stallIds =
                    uniqueIds(
                        placements.map(
                            function ( row ) {
                                return row.stall_id;
                            }
                        )
                    );

                const festivalIds =
                    uniqueIds(
                        placements.map(
                            function ( row ) {
                                return row.festival_id;
                            }
                        )
                    );

                const venueIds =
                    uniqueIds(
                        placements.map(
                            function ( row ) {
                                return row.venue_id;
                            }
                        )
                    );


                return Promise.all( [

                    stallIds.length
                        ? cargoQuery(

                            'Stalls',

                            'stall_id=stall_id,' +
                            'name=stall_name,' +
                            '_pageName=page_name',

                            'stall_id IN (' +
                            makeInClause(
                                stallIds
                            ) +
                            ')',

                            100

                        )
                        : Promise.resolve( [] ),


                    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,' +
                            '_pageName=page_name',

                            'venue_id IN (' +
                            makeInClause(
                                venueIds
                            ) +
                            ')',

                            100

                        )
                        : Promise.resolve( [] )

                ] ).then(
                    function ( related ) {

                        return {

                            placements:
                                placements,

                            stalls:
                                related[ 0 ],

                            festivals:
                                related[ 1 ],

                            venues:
                                related[ 2 ]

                        };

                    }
                );

            }
        ).then(
            function ( data ) {

                const stallMap =
                    mapBy(
                        data.stalls,
                        'stall_id'
                    );

                const festivalMap =
                    mapBy(
                        data.festivals,
                        'festival_id'
                    );

                const venueMap =
                    mapBy(
                        data.venues,
                        'venue_id'
                    );


                data.placements.forEach(
                    function ( placement ) {

                        const placementId =
                            String(
                                placement.placement_id
                            );

                        const info = {

                            placement:
                                placement,

                            stall:
                                stallMap[
                                    String(
                                        placement.stall_id
                                    )
                                ] || null,

                            festival:
                                festivalMap[
                                    String(
                                        placement.festival_id
                                    )
                                ] || null,

                            venue:
                                venueMap[
                                    String(
                                        placement.venue_id
                                    )
                                ] || null

                        };


                        placementInfoCache[
                            placementId
                        ] = info;

                        result[
                            placementId
                        ] = info;

                    }
                );


                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 )
        );

    }


    /* =====================================
     * Placement比較ボタン生成
     * ===================================== */

    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 placementId =
                        placeholder.id.substring(
                            prefix.length
                        );


                    if (
                        !/^\d+$/.test(
                            placementId
                        ) ||
                        placementId === '0'
                    ) {
                        return;
                    }


                    const button =
                        document.createElement(
                            'button'
                        );

                    button.type =
                        'button';

                    button.className =
                        'stall-compare-button';

                    button.dataset.placementId =
                        placementId;

                    button.setAttribute(
                        'aria-pressed',
                        'false'
                    );

                    button.textContent =
                        '比較に追加';


                    placeholder.appendChild(
                        button
                    );

                    placeholder.dataset.initialized =
                        '1';

                }
            );

    }


    /* =====================================
     * ボタン状態更新
     * ===================================== */

    function updateCompareButtons() {

        const ids =
            getComparePlacements();


        document
            .querySelectorAll(
                '.stall-compare-button'
            )
            .forEach(
                function ( button ) {

                    const placementId =
                        String(
                            button.dataset
                                .placementId || ''
                        );


                    const selected =
                        ids.includes(
                            placementId
                        );


                    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 =
            getComparePlacements();


        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;

        }


        if ( ids.length === 0 ) {

            tray.classList.remove(
                'stall-compare-tray-visible'
            );

            if ( items ) {
                items.innerHTML = '';
            }

            return;

        }


        tray.classList.add(
            'stall-compare-tray-visible'
        );


        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 =
            '屋台情報を読み込み中…';


        fetchPlacementInfo(
            ids
        ).then(
            function ( placementInfo ) {

                const currentIds =
                    getComparePlacements();

                items.innerHTML =
                    '';


                currentIds.forEach(
                    function ( placementId ) {

                        const info =
                            placementInfo[
                                placementId
                            ];


                        if ( !info ) {
                            return;
                        }


                        const item =
                            document.createElement(
                                'div'
                            );

                        item.className =
                            'stall-compare-tray-item';


                        const text =
                            document.createElement(
                                'div'
                            );


                        const name =
                            document.createElement(
                                info.stall &&
                                info.stall.page_name
                                    ? 'a'
                                    : 'span'
                            );


                        if (
                            info.stall &&
                            info.stall.page_name
                        ) {

                            name.href =
                                mw.util.getUrl(
                                    info.stall.page_name
                                );

                        }


                        name.className =
                            'stall-compare-tray-item-name';

                        name.textContent =
                            info.stall
                                ? info.stall.stall_name
                                : '屋台';


                        const context =
                            document.createElement(
                                'div'
                            );

                        context.className =
                            'stall-compare-tray-item-context';


                        const parts = [];


                        if (
                            info.placement &&
                            info.placement.year
                        ) {

                            parts.push(
                                info.placement.year +
                                '年'
                            );

                        }


                        if (
                            info.festival &&
                            info.festival.festival_name
                        ) {

                            parts.push(
                                info.festival
                                    .festival_name
                            );

                        }


                        if (
                            info.venue &&
                            info.venue.venue_name
                        ) {

                            parts.push(
                                info.venue
                                    .venue_name
                            );

                        }


                        context.textContent =
                            parts.join(
                                ' / '
                            );


                        text.appendChild(
                            name
                        );

                        text.appendChild(
                            context
                        );


                        const removeButton =
                            document.createElement(
                                'button'
                            );

                        removeButton.type =
                            'button';

                        removeButton.className =
                            'stall-compare-tray-remove';

                        removeButton.dataset.placementId =
                            placementId;

                        removeButton.setAttribute(
                            'aria-label',
                            (
                                info.stall
                                    ? info.stall.stall_name
                                    : '屋台'
                            ) +
                            'を比較候補から外す'
                        );

                        removeButton.textContent =
                            '×';


                        item.appendChild(
                            text
                        );

                        item.appendChild(
                            removeButton
                        );

                        items.appendChild(
                            item
                        );

                    }
                );

            }
        ).catch(
            function ( error ) {

                console.error(
                    '比較トレイ取得エラー:',
                    error
                );

                items.textContent =
                    '比較候補を読み込めませんでした。';

            }
        );

    }


    /* =====================================
     * 詳細ページ
     * 「比較に追加」
     * ===================================== */

    document.addEventListener(
        'click',
        function ( event ) {

            const button =
                event.target.closest(
                    '.stall-compare-button'
                );

            if ( !button ) {
                return;
            }


            event.preventDefault();


            const placementId =
                String(
                    button.dataset
                        .placementId || ''
                );


            if (
                !/^\d+$/.test(
                    placementId
                )
            ) {
                return;
            }


            const control =
                button.closest(
                    '.stall-compare-control'
                );


            let ids =
                getComparePlacements();


            const index =
                ids.indexOf(
                    placementId
                );


            /*
             * すでに選択中
             */
            if ( index !== -1 ) {

                ids.splice(
                    index,
                    1
                );

                saveComparePlacements(
                    ids
                );

                updateCompareButtons();
                updateCompareTray();


                if ( control ) {

                    showMessage(
                        control,
                        '比較候補から外しました。',
                        false
                    );

                }

                return;

            }


            /*
             * 最大4件
             */
            if (
                ids.length >=
                MAX_COMPARE
            ) {

                if ( control ) {

                    showMessage(
                        control,
                        '比較できる出店は最大4件です。',
                        true
                    );

                }

                return;

            }


            ids.push(
                placementId
            );


            saveComparePlacements(
                ids
            );


            updateCompareButtons();
            updateCompareTray();


            if ( control ) {

                showMessage(
                    control,
                    '比較候補に追加しました(' +
                    ids.length +
                    '/4)。',
                    false
                );

            }

        }
    );


    /* =====================================
     * トレイから1件削除
     * ===================================== */

    document.addEventListener(
        'click',
        function ( event ) {

            const button =
                event.target.closest(
                    '.stall-compare-tray-remove'
                );

            if ( !button ) {
                return;
            }


            event.preventDefault();


            const placementId =
                String(
                    button.dataset
                        .placementId || ''
                );


            let ids =
                getComparePlacements();


            ids =
                ids.filter(
                    function ( id ) {
                        return id !== placementId;
                    }
                );


            saveComparePlacements(
                ids
            );


            updateCompareButtons();
            updateCompareTray();

        }
    );


    /* =====================================
     * 全削除
     * ===================================== */

    document.addEventListener(
        'click',
        function ( event ) {

            const button =
                event.target.closest(
                    '.stall-compare-clear'
                );

            if ( !button ) {
                return;
            }


            event.preventDefault();


            saveComparePlacements(
                []
            );


            updateCompareButtons();
            updateCompareTray();

        }
    );


    /* =====================================
     * 1件時の比較リンク無効
     * ===================================== */

    document.addEventListener(
        'click',
        function ( event ) {

            const link =
                event.target.closest(
                    '.stall-compare-open-disabled'
                );

            if ( !link ) {
                return;
            }

            event.preventDefault();

        }
    );


    /* =====================================
     * 初期化
     * ===================================== */

    function initPlacementCompare() {

        createCompareButtons();

        createCompareTray();

        updateCompareButtons();

        updateCompareTray();

    }


    initPlacementCompare();


    mw.hook(
        'wikipage.content'
    ).add(
        function () {

            initPlacementCompare();

        }
    );

} );

/* ========================================
 * 屋台比較ページ
 * placement_id 正式版
 * ======================================== */

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 =
        'matsuriWikiComparePlacements';

    const MIN_COMPARE = 2;
    const MAX_COMPARE = 4;

    const api =
        new mw.Api();


    /* =====================================
     * localStorage
     * ===================================== */

    function getPlacementIds() {

        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
     * ===================================== */

    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
                        );

                    }
                );

            }
        );

    }


    function makeInClause( ids ) {

        return ids
            .map( String )
            .filter(
                function ( id ) {
                    return /^\d+$/.test(
                        id
                    );
                }
            )
            .join( ',' );

    }


    function uniqueIds( values ) {

        return [
            ...new Set(
                values
                    .map( String )
                    .filter(
                        function ( id ) {

                            return (
                                id &&
                                /^\d+$/.test(
                                    id
                                )
                            );

                        }
                    )
            )
        ];

    }


    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 '未確認';

        }

    }


    /* =====================================
     * 単位価格
     * ===================================== */

    function getUnitPrice(
        offering
    ) {

        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
     * ===================================== */

    function createTextCell(
        tagName,
        text
    ) {

        const cell =
            document.createElement(
                tagName
            );

        cell.textContent =
            text;

        return cell;

    }


    /* =====================================
     * メニュー
     * ===================================== */

    function createMenuList(
        menus
    ) {

        const container =
            document.createElement(
                'div'
            );

        container.className =
            'stall-compare-menu-list';


        if (
            !menus ||
            menus.length === 0
        ) {

            container.textContent =
                'メニュー未登録';

            return container;

        }


        menus.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 unit =
                    document.createElement(
                        'div'
                    );

                unit.textContent =
                    '1単位あたり:' +
                    item.unitPrice;


                const availability =
                    document.createElement(
                        'div'
                    );

                availability.textContent =
                    '販売状況:' +
                    item.availability;


                menu.appendChild(
                    name
                );

                menu.appendChild(
                    price
                );

                menu.appendChild(
                    serving
                );

                menu.appendChild(
                    unit
                );

                menu.appendChild(
                    availability
                );


                container.appendChild(
                    menu
                );

            }
        );


        return container;

    }


    /* =====================================
     * 比較表
     * ===================================== */

    function renderComparison(
        compareData
    ) {

        compareRoot.innerHTML =
            '';


        const heading =
            document.createElement(
                'h2'
            );

        heading.textContent =
            '屋台比較';


        compareRoot.appendChild(
            heading
        );


        const wrapper =
            document.createElement(
                'div'
            );

        wrapper.className =
            'stall-compare-table-wrapper';


        const table =
            document.createElement(
                'table'
            );

        table.className =
            'stall-compare-table';


        /* ------------------------------
         * thead
         * ------------------------------ */

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

                    tr.appendChild(
                        createTextCell(
                            'td',
                            textOrDash(
                                getter(
                                    data
                                )
                            )
                        )
                    );

                }
            );


            tbody.appendChild(
                tr
            );

        }


        /* =================================
         * Placement情報
         * ================================= */

        addRow(
            '開催年',
            function ( data ) {

                return data.placement
                    ? data.placement.year +
                      '年'
                    : '―';

            }
        );


        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
                    )
                    : '―';

            }
        );


        /* =================================
         * メニュー
         * ================================= */

        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 =
            'この比較は出店単位(Placement)です。祭り・開催年・会場ごとの価格、営業時間、出店位置を比較しています。';


        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 placementIds =
        getPlacementIds();


    if (
        placementIds.length <
        MIN_COMPARE
    ) {

        renderMessage(
            '比較する出店を2件以上選択してください。'
        );

        return;

    }


    /*
     * STEP 1
     * Placementを直接取得
     */
    cargoQuery(

        'FestivalStallPlacements',

        'placement_id=placement_id,' +
        'stall_id=stall_id,' +
        'festival_id=festival_id,' +
        'venue_id=venue_id,' +
        'year=year,' +
        'latitude=latitude,' +
        'longitude=longitude,' +
        'location_note=location_note,' +
        'opening_time=opening_time,' +
        'closing_time=closing_time,' +
        'hours_note=hours_note,' +
        'position_status=position_status,' +
        'verification_status=verification_status',

        'placement_id IN (' +
        makeInClause(
            placementIds
        ) +
        ')',

        100

    ).then(
        function ( placements ) {

            const stallIds =
                uniqueIds(
                    placements.map(
                        function ( row ) {
                            return row.stall_id;
                        }
                    )
                );


            const festivalIds =
                uniqueIds(
                    placements.map(
                        function ( row ) {
                            return row.festival_id;
                        }
                    )
                );


            const venueIds =
                uniqueIds(
                    placements.map(
                        function ( row ) {
                            return row.venue_id;
                        }
                    )
                );


            /*
             * STEP 2
             */
            return Promise.all( [

                stallIds.length
                    ? cargoQuery(

                        'Stalls',

                        'stall_id=stall_id,' +
                        'name=stall_name,' +
                        'category=category,' +
                        '_pageName=page_name',

                        'stall_id IN (' +
                        makeInClause(
                            stallIds
                        ) +
                        ')',

                        100

                    )
                    : Promise.resolve(
                        []
                    ),


                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(
                        []
                    ),


                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

                )

            ] ).then(
                function ( results ) {

                    return {

                        placements:
                            placements,

                        stalls:
                            results[ 0 ],

                        festivals:
                            results[ 1 ],

                        venues:
                            results[ 2 ],

                        offerings:
                            results[ 3 ]

                    };

                }
            );

        }
    ).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
             */
            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 ( results ) {

                    data.areas =
                        results[ 0 ];

                    data.menuItems =
                        results[ 1 ];

                    return data;

                }
            );

        }
    ).then(
        function ( data ) {

            const placementMap =
                mapBy(
                    data.placements,
                    'placement_id'
                );


            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 =
                placementIds.map(
                    function (
                        placementId
                    ) {

                        const placement =
                            placementMap[
                                placementId
                            ] || null;


                        if ( !placement ) {

                            return {

                                placementId:
                                    placementId,

                                placement:
                                    null,

                                stall:
                                    null,

                                festival:
                                    null,

                                venue:
                                    null,

                                area:
                                    null,

                                menus:
                                    []

                            };

                        }


                        const stall =
                            stallMap[
                                String(
                                    placement.stall_id
                                )
                            ] || null;


                        const festival =
                            festivalMap[
                                String(
                                    placement.festival_id
                                )
                            ] || null;


                        const venue =
                            venueMap[
                                String(
                                    placement.venue_id
                                )
                            ] || null;


                        let area = null;


                        if (
                            venue &&
                            venue.area_id
                        ) {

                            area =
                                areaMap[
                                    String(
                                        venue.area_id
                                    )
                                ] || null;

                        }


                        const offerings =
                            offeringsByPlacement[
                                placementId
                            ] || [];


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

                            placementId:
                                placementId,

                            placement:
                                placement,

                            stall:
                                stall,

                            festival:
                                festival,

                            venue:
                                venue,

                            area:
                                area,

                            menus:
                                menus

                        };

                    }
                );


            renderComparison(
                compareData
            );

        }
    ).catch(
        function ( error ) {

            console.error(
                'Placement比較データ取得エラー:',
                error
            );


            renderMessage(
                '比較データの取得中にエラーが発生しました。'
            );

        }
    );

} );