Greasy Fork is available in English.

碧蓝 wiki 大型作战成就记录地图增强

增加侵蚀度、成就奖励、海域id、海域名称、成就种类、档案筛选器,添加存/读档功能,避免弹出框偏移出地图,去除平移与缩放

// ==UserScript==
// @name        碧蓝 wiki 大型作战成就记录地图增强
// @namespace   http://github.com/8qwe24657913
// @match       https://wiki.biligame.com/blhx/%E5%A4%A7%E5%9E%8B%E4%BD%9C%E6%88%98%E6%88%90%E5%B0%B1%E8%AE%B0%E5%BD%95%E5%9C%B0%E5%9B%BE
// @grant       none
// @run-at      document-start
// @version     1.1.2
// @author      8q
// @description 增加侵蚀度、成就奖励、海域id、海域名称、成就种类、档案筛选器,添加存/读档功能,避免弹出框偏移出地图,去除平移与缩放
// ==/UserScript==

/* globals L mapData mapModel filterMouseover filterMouseout mapPoints filterClick updateSection mapSize updateMap saveAchievements normalAchievementKeywords safeAchievementKeywords */
;(function () {
    'use strict'
    function patch() {
        const LEVEL_COUNT = 6
        const ALL_REWARDS = [
            '深渊6',
            '深渊5',
            '深渊4',
            '宝箱6',
            '金菜',
            '紫菜',
            '金猫箱',
            '魔方',
            '紫币',
            '物资',
            '指令书',
        ]
        const MAP_DATA_SUPPLEMENT = {
            11: [6, '指令书', '金菜', '魔方', '深渊6', '金猫箱'],
            12: [5, '指令书', '物资', '魔方', '紫币', '金猫箱'],
            13: [5, '指令书', '金菜', '魔方', '紫币', '金猫箱'],
            14: [4, '紫币', '物资', '金菜', '紫币', '魔方'],
            21: [2, '紫币', '物资', '紫菜', '紫币', '金菜'],
            22: [1, '紫币', '指令书', '紫菜', '紫币', '物资'],
            23: [2, '指令书', '紫菜', '紫菜', '紫币', '金菜'],
            24: [2, '指令书', '紫菜', '物资', '紫币', '物资'],
            25: [3, '指令书', '紫菜', '金菜', '紫币', '金菜'],
            31: [2, '紫币', '紫菜', '紫菜', '紫币', '物资'],
            32: [3, '指令书', '紫菜', '金菜', '宝箱6', '金菜'],
            33: [3, '指令书', '紫菜', '物资', '宝箱6', '金菜'],
            34: [3, '指令书', '紫菜', '紫菜', '宝箱6', '魔方'],
            41: [3, '指令书', '物资', '金菜', '紫币', '魔方'],
            42: [4, '指令书', '物资', '金菜', '紫币', '魔方'],
            43: [2, '紫币', '紫菜', '物资', '紫币', '物资'],
            44: [1, '紫币', '指令书', '紫菜', '紫币', '物资'],
            51: [4, '指令书', '物资', '魔方', '深渊4', '魔方'],
            52: [4, '紫币', '金菜', '魔方', '紫币', '魔方'],
            53: [4, '指令书', '金菜', '魔方', '紫币', '魔方'],
            54: [4, '指令书', '金菜', '金菜', '深渊4', '魔方'],
            61: [4, '指令书', '物资', '魔方', '深渊4', '魔方'],
            62: [3, '指令书', '物资', '金菜', '宝箱6', '紫菜'],
            63: [4, '指令书', '物资', '金菜', '紫币', '魔方'],
            64: [4, '指令书', '金菜', '金菜', '深渊4', '魔方'],
            65: [3, '指令书', '指令书', '金菜', '紫币', '魔方'],
            66: [3, '指令书', '指令书', '物资', '紫币', '魔方'],
            71: [5, '指令书', '金菜', '魔方', '深渊5', '金猫箱'],
            72: [6, '指令书', '金菜', '魔方', '深渊6', '金猫箱'],
            73: [5, '指令书', '金菜', '魔方', '深渊5', '金猫箱'],
            81: [2, '指令书', '指令书', '物资', '紫币', '金菜'],
            82: [4, '紫币', '金菜', '魔方', '深渊4', '魔方'],
            83: [2, '指令书', '物资', '物资', '紫币', '金菜'],
            84: [2, '紫币', '紫菜', '物资', '紫币', '金菜'],
            85: [4, '指令书', '金菜', '魔方', '深渊4', '魔方'],
            91: [4, '指令书', '金菜', '魔方', '深渊4', '魔方'],
            92: [2, '紫币', '指令书', '紫菜', '紫币', '物资'],
            93: [2, '指令书', '指令书', '紫菜', '紫币', '物资'],
            94: [3, '指令书', '指令书', '物资', '宝箱6', '紫菜'],
            95: [3, '指令书', '金菜', '紫菜', '宝箱6', '紫菜'],
            101: [5, '指令书', '物资', '魔方', '深渊5', '金猫箱'],
            102: [5, '指令书', '物资', '魔方', '深渊5', '金猫箱'],
            103: [4, '紫币', '金菜', '魔方', '深渊4', '魔方'],
            104: [4, '紫币', '金菜', '魔方', '深渊4', '魔方'],
            105: [3, '紫币', '指令书', '紫菜', '宝箱6', '魔方'],
            106: [6, '指令书', '金菜', '魔方', '深渊6', '金猫箱'],
            111: [3, '紫币', '指令书', '金菜', '紫币', '魔方'],
            112: [2, '指令书', '指令书', '紫菜', '紫币', '物资'],
            113: [3, '紫币', '指令书', '金菜', '紫币', '魔方'],
            114: [3, '紫币', '指令书', '物资', '宝箱6', '物资'],
            121: [6, '指令书', '金菜', '魔方', '深渊6', '金猫箱'],
            122: [2, '指令书', '物资', '紫菜', '紫币', '物资'],
            123: [3, '紫币', '金菜', '紫菜', '紫币', '物资'],
            124: [5, '指令书', '物资', '魔方', '紫币', '金猫箱'],
            125: [3, '紫币', '指令书', '紫菜', '紫币', '物资'],
            131: [2, '指令书', '指令书', '紫菜', '紫币', '物资'],
            132: [2, '指令书', '物资', '紫菜', '紫币', '物资'],
            133: [3, '紫币', '金菜', '金菜', '紫币', '物资'],
            134: [2, '指令书', '物资', '紫菜', '紫币', '物资'],
            135: [3, '紫币', '金菜', '物资', '宝箱6', '魔方'],
            141: [3, '紫币', '指令书', '金菜', '紫币', '魔方'],
            142: [4, '紫币', '金菜', '魔方', '深渊4', '魔方'],
            143: [3, '紫币', '指令书', '金菜', '宝箱6', '魔方'],
            144: [5, '指令书', '金菜', '魔方', '紫币', '金猫箱'],
            151: [5, '指令书', '金菜', '魔方', '深渊5', '金猫箱'],
            152: [5, '指令书', '金菜', '魔方', '紫币', '金猫箱'],
            153: [6, '指令书', '金菜', '魔方', '深渊6', '金猫箱'],
            155: [6, '指令书', '金菜', '魔方', '深渊6', '金猫箱'],
            156: [6, '指令书', '金菜', '魔方', '深渊6', '金猫箱'],
            157: [6, '指令书', '金菜', '魔方', '深渊6', '金猫箱'],
            158: [5, '指令书', '物资', '魔方', '紫币', '金猫箱'],
            159: [5, '指令书', '物资', '魔方', '深渊5', '金猫箱'],
        }
        const FILES = {
            陨石事件: [44, 84, 125, 95, 104, 52],
            能源革命: [22, 134, 32, 94, 142, 53],
            科技与生活: [83, 122, 135, 143, 63, 54],
            生活的变革: [23, 62, 114, 51, 41, 14],
            魔方军用化: [21, 31, 66, 113, 65, 85],
            魔方军用化II: [43, 112, 34, 133, 61, 71],
            '「微光」计划': [81, 132, 123, 105, 91, 64],
            军备竞赛: [24, 92, 111, 25, 82, 42],
            冷战升级: [93, 131, 141, 33, 103, 13],
        }
        const NORMAL_FILE = '档案(1/3/5)'
        const SAFE_FILE = '档案(2/4/6)'
        const SELECTOR_CLASS = 'leaflet-control-layers-selector'
        const name2file = {}
        for (const [file, ids] of Object.entries(FILES)) {
            for (const [i, id] of ids.entries()) {
                if (!mapData[id]) continue
                const name = mapData[id][0]
                name2file[name] = [file, i + 1]
            }
        }
        const name2id = {}
        const name2level = {}
        const name2rewards = {}
        const achievementTypes = {
            普通海域: [...normalAchievementKeywords, NORMAL_FILE],
            安全海域: [...safeAchievementKeywords, SAFE_FILE],
        }
        const isSafe = {}
        for (const [safety, achievementType] of Object.entries(achievementTypes)) {
            for (const achievement of achievementType) {
                isSafe[achievement] = safety === '安全海域'
            }
        }
        const name2AchievementTypes = {}
        const achievementKeywordsWithoutFile = [...normalAchievementKeywords, ...safeAchievementKeywords]
        for (const [id, [level, ...rewards]] of Object.entries(MAP_DATA_SUPPLEMENT)) {
            if (!mapData[id]) continue
            const name = mapData[id][0]
            name2id[name] = Number(id)
            name2level[name] = level
            name2rewards[name] = rewards
            name2AchievementTypes[name] = mapData[id].slice(3).map((achievement) => {
                if (achievement.includes('档案')) {
                    return name2file[name][1] % 2 === 0 ? SAFE_FILE : NORMAL_FILE
                } else {
                    return achievementKeywordsWithoutFile.find((keyword) => achievement.includes(keyword))
                }
            })
        }
        // 原来就有的 bug,更新了成就情况后筛选结果不跟着变
        const oldUpdateMap = updateMap
        window.updateMap = function updateMap() {
            oldUpdateMap()
            updateSection()
        }
        // 增加各种筛选器
        const safetyFilterList = L.DomUtil.get('filter-safe-todo-box').parentElement
        const safetyFilterChildren = [...safetyFilterList.children]
        function createFilterList(type, text) {
            L.DomUtil.create(
                'div',
                `filter-title filter-${type}-title`,
                safetyFilterList.parentElement,
            ).innerText = text
            return L.DomUtil.create('div', `filter-list filter-${type}-list`, safetyFilterList.parentElement)
        }
        function createFilterBox(parent, type, text = '') {
            const filterBox = L.DomUtil.create('label', null, parent)
            filterBox.id = `filter-${type}-box`
            filterBox.for = `filter-${type}`
            if (text) filterBox.innerText = text
            return filterBox
        }
        function createFilterCheckbox(parent, type, text) {
            const filterBox = createFilterBox(parent, type)
            filterBox.innerHTML = `<div><input type="checkbox" id="filter-${type}" class="${SELECTOR_CLASS}"><span>${text}</span></div>`
            return L.DomUtil.get(`filter-${type}`)
        }
        function createOption(parent, value, text = value) {
            const option = L.DomUtil.create('option', null, parent)
            option.value = value
            option.innerText = text
            return option
        }
        function createSelect(parent) {
            const filterSelect = L.DomUtil.create('select', SELECTOR_CLASS, parent)
            createOption(filterSelect, '', '(未选择)')
            return filterSelect
        }
        // 侵蚀度
        const levelFilterList = createFilterList('level', '侵蚀度:')
        const levelFilterBoxes = new Array(LEVEL_COUNT)
            .fill()
            .map((_, i) => createFilterCheckbox(levelFilterList, `level-${i + 1}`, `侵蚀${i + 1}`))
        // 未获取的奖励
        const rewardFilterList = createFilterList('reward', '未获取的奖励:')
        const rewardFilterBoxes = ALL_REWARDS.map((reward, i) =>
            createFilterCheckbox(rewardFilterList, `reward-${i + 1}`, reward),
        )
        // 海域搜索
        const searchFilterList = createFilterList('name-id', '海域搜索:')
        const searchFilterBoxes = [
            ['name', '海域名称:', null],
            ['id', '海域编号:', '^\\d+$'],
        ].map(([type, text, pattern]) => {
            const filterBox = createFilterBox(searchFilterList, type, text)
            const filterInput = L.DomUtil.create('input', SELECTOR_CLASS, filterBox)
            filterInput.type = 'text'
            filterInput.id = `filter-${type}`
            if (pattern) filterInput.pattern = pattern
            return filterInput
        })
        // 档案
        const fileFilterBoxes = [
            ['file-name', '档案名称:', Object.keys(FILES)],
            ['file-index', '档案序号:', [1, 2, 3, 4, 5, 6]],
        ].map(([type, text, options]) => {
            const filterBox = createFilterBox(searchFilterList, type, text)
            const filterSelect = createSelect(filterBox)
            for (const option of options) {
                createOption(filterSelect, option)
            }
            return filterSelect
        })
        // 未完成成就具体类型
        const achievementTypeSelect = (() => {
            const filterBox = createFilterBox(safetyFilterList, 'achievement', '具体类型:')
            const filterSelect = createSelect(filterBox)
            for (const [group, types] of Object.entries(achievementTypes)) {
                const optgroup = L.DomUtil.create('optgroup', null, filterSelect)
                optgroup.label = group
                for (const type of types) {
                    createOption(optgroup, type)
                }
            }
            return filterSelect
        })()
        const filters = {
            safety: {
                checked: false,
                // 原来的筛选函数里没有考虑档案,这里修个 bug
                filter(section) {
                    const achievementTypes = name2AchievementTypes[section.name]
                    for (const [i, isCompleted] of section.completed.entries()) {
                        if (isCompleted) continue
                        if (isSafe[achievementTypes[i]] ? mapModel.filters.safeTodo : mapModel.filters.normalTodo)
                            return true
                    }
                    return false
                },
                update() {
                    mapModel.filters.safeTodo = L.DomUtil.get('filter-safe-todo').checked
                    mapModel.filters.normalTodo = L.DomUtil.get('filter-normal-todo').checked
                    this.checked = mapModel.filters.safeTodo || mapModel.filters.normalTodo
                },
            },
            level: {
                checked: false,
                filter(section) {
                    return mapModel.filters.levels[name2level[section.name] - 1]
                },
                update() {
                    mapModel.filters.levels = levelFilterBoxes.map((checkbox) => checkbox.checked)
                    this.checked = mapModel.filters.levels.includes(true)
                },
            },
            reward: {
                checked: false,
                filter(section) {
                    const rewards = name2rewards[section.name].slice(section.completed.filter((x) => x).length)
                    return rewards.some((reward) => mapModel.filters.rewards.has(reward))
                },
                update() {
                    mapModel.filters.rewards = new Set(
                        rewardFilterBoxes.map((checkbox, i) => checkbox.checked && ALL_REWARDS[i]).filter((x) => x),
                    )
                    this.checked = mapModel.filters.rewards.size > 0
                },
            },
            name: {
                checked: false,
                filter(section) {
                    return section.name.includes(mapModel.filters.name)
                },
                update() {
                    mapModel.filters.name = searchFilterBoxes[0].value
                    this.checked = !!mapModel.filters.name
                },
            },
            id: {
                checked: false,
                filter(section) {
                    return name2id[section.name] === mapModel.filters.id
                },
                update() {
                    const value = searchFilterBoxes[1].value
                    mapModel.filters.id = Number(value)
                    this.checked = !!value && !Number.isNaN(mapModel.filters.id)
                },
            },
            achievement: {
                checked: false,
                filter(section) {
                    const achievementTypes = name2AchievementTypes[section.name]
                    for (const [i, isCompleted] of section.completed.entries()) {
                        if (isCompleted) continue
                        if (achievementTypes[i] === mapModel.filters.achievementType) return true
                    }
                    return false
                },
                update() {
                    mapModel.filters.achievementType = achievementTypeSelect.value
                    this.checked = !!mapModel.filters.achievementType
                },
            },
            fileType: {
                checked: false,
                filter(section) {
                    return name2file[section.name] && name2file[section.name][0] === mapModel.filters.fileType
                },
                update() {
                    mapModel.filters.fileType = fileFilterBoxes[0].value
                    this.checked = !!mapModel.filters.fileType
                },
            },
            fileIndex: {
                checked: false,
                filter(section) {
                    return name2file[section.name] && name2file[section.name][1] === mapModel.filters.fileIndex
                },
                update() {
                    mapModel.filters.fileIndex = Number(fileFilterBoxes[1].value)
                    this.checked = !!mapModel.filters.fileIndex
                },
            },
        }
        /**
         * 筛选规则:
         * 1. 同一筛选器的不同选项间为逻辑或关系,不同筛选器间为逻辑与关系
         * 2. 若用户未填写某个筛选器,则视为该筛选器不存在
         * 3. 不存在任何筛选器时,不选中任何区块
         */
        let hasFilter = false
        window.filterSection = function filterSection(section) {
            if (!hasFilter) return false
            for (const { filter, checked } of Object.values(filters)) {
                if (checked && !filter(section)) return false
            }
            return true
        }
        function filterChange() {
            hasFilter = false
            for (const filter of Object.values(filters)) {
                filter.update()
                hasFilter = hasFilter || filter.checked
            }
            updateSection()
        }
        filterChange()
        function newFilterMouseover(checkbox) {
            if (!checkbox.checked) {
                checkbox.checked = true
                filterChange()
                checkbox.checked = false
            }
        }
        L.DomEvent.off(safetyFilterList, 'click', filterClick)
        for (const box of safetyFilterList.children) {
            L.DomEvent.off(box, {
                mouseover: filterMouseover,
                mouseout: filterMouseout,
            })
        }
        for (const box of [...safetyFilterChildren, ...levelFilterList.children, ...rewardFilterList.children]) {
            L.DomEvent.on(box, {
                mouseover: newFilterMouseover.bind(null, L.DomUtil.get(box.id.slice(0, -4))),
                mouseout: filterChange,
                change: filterChange,
            })
        }
        for (const box of searchFilterBoxes) {
            L.DomEvent.on(box, 'input paste change', filterChange)
        }
        for (const box of [achievementTypeSelect, ...fileFilterBoxes]) {
            L.DomEvent.on(box, 'change', filterChange)
        }
        // 备份 / 读取存档
        const collapseList = document.getElementsByClassName('leaflet-control-layers-overlays')[0].parentElement
        L.DomUtil.create('div', 'leaflet-control-layers-separator', collapseList)
        const saveLoadList = L.DomUtil.create('div', 'achievement-backup-load', collapseList)
        const saveLoad = [
            {
                type: 'backup',
                text: '备份',
                onclick() {
                    const achievementList = []
                    for (const i in mapModel.mapSection) {
                        const section = mapModel.mapSection[i]
                        for (const j in section.completed) {
                            if (section.completed[j]) {
                                achievementList.push(i + '-' + j)
                            }
                        }
                    }
                    const userAchievements = achievementList.join(',')
                    const blob = new Blob([userAchievements], {
                        type: 'text/plain',
                    })
                    const a = document.createElement('a')
                    a.href = URL.createObjectURL(blob)
                    a.download = `achievements-${new Date().toISOString()}.txt`
                    a.click()
                },
            },
            {
                type: 'load',
                text: '读档',
                onclick() {
                    const input = L.DomUtil.create('input')
                    input.type = 'file'
                    input.accept = '.txt,text/plain'
                    input.onchange = () => {
                        if (input.files.length === 0) return
                        const [file] = input.files
                        const fr = new FileReader()
                        fr.onload = () => {
                            updateAchievements(fr.result)
                            saveAchievements()
                        }
                        fr.readAsText(file)
                    }
                    input.click()
                },
            },
        ]
        for (const { type, text, onclick } of saveLoad) {
            const button = L.DomUtil.create('button', `achievement-${type}`, saveLoadList)
            button.innerText = text
            L.DomEvent.on(button, 'click', onclick)
        }
        // 修改 popup 位置,使其不会超出窗口外
        initRrose()
        for (const mapPoint of Object.values(mapPoints)) {
            const { marker, popup } = mapPoint
            marker.bindPopup(
                (mapPoint.popup = new L.Rrose({
                    ...popup.options,
                    autoPan: false,
                }).setContent(popup.getContent())),
            )
            popup.remove()
        }
        // 防止用户手快事先平移或缩放了,复位一下
        const map = mapPoints[11].marker._map
        map.fitBounds([[0, 0], mapSize])
        // 禁止平移和缩放
        map.boxZoom.disable()
        map.doubleClickZoom.disable()
        map.dragging.disable()
        map.keyboard.disable()
        map.scrollWheelZoom.disable()
        map.touchZoom.disable()
        map.zoomControl.remove()
        // 自定义样式
        document.head.appendChild(document.createElement('style')).appendChild(
            document.createTextNode(`
            #filter-name {
                max-width: 90px;
            }
            #filter-id {
                max-width: 30px;
            }
            #alworldmap input:invalid {
                border-color: rgba(255, 0, 0, 0.5);
            }
            /* 存/读档按钮 */
            .achievement-backup-load {
                display: flex;
                justify-content: space-around;
            }
            /* 防止用户选中奇怪的东西 */
            #alworldmap {
                user-select: none;
            }
            .leaflet-rrose-content, .achievement-count-text {
                user-select: text;
            }
            /* 能点的东西给个提示啊 */
            #alworldmap .achievement,
            #alworldmap label,
            #alworldmap input[type="checkbox"],
            #alworldmap input[type="radio"],
            .leaflet-rrose-close-button,
            .achievement-backup-load button {
                cursor: pointer;
            }
        `),
        )
    }
    // 实测 bwiki 上 media wiki api 存的东西有时会乱套(读到了别人的存档?),先把网络同步禁用掉
    function loadAchievements() {
        return localStorage.getItem('userjs-worldmap-achievements') || ''
    }
    function updateAchievements(achievements) {
        const updated = new Set()
        for (const achievement of achievements.split(',')) {
            const [i, j] = achievement.split('-')
            const { completed } = mapModel.mapSection[i]
            if (!updated.has(i)) {
                completed.fill(false)
                updated.add(i)
            }
            completed[j] = true
        }
        updateMap()
    }
    // eslint-disable-next-line no-unused-vars
    function replaceLoadAchievements() {
        window.loadAchievements = loadAchievements
    }
    if (typeof mapModel === 'undefined') {
        // 早于地图初始化
        Object.defineProperty(window, 'mapModel', {
            get() {
                return undefined
            },
            set(value) {
                Object.defineProperty(window, 'mapModel', {
                    value,
                    writable: true,
                    enumerable: true,
                    configurable: true,
                })
                // replaceLoadAchievements()
                // 等初始化完了再执行 patch
                queueMicrotask(patch)
            },
            enumerable: false,
            configurable: true,
        })
    } else {
        // 晚于地图初始化
        // replaceLoadAchievements()
        // 执行时机晚则需要手动读取本地存档以更新地图
        // updateAchievements(loadAchievements())
        patch()
    }
    // 下面是引用的库,用来调整弹出框位置
    function initRrose() {
        /*
  Copyright (c) 2012 Eric S. Theise
  
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 
  documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 
  rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 
  persons to whom the Software is furnished to do so, subject to the following conditions:
  
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 
  Software.
  
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 
  WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 
  COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 
  OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

        L.Rrose = L.Popup.extend({
            _initLayout: function () {
                const prefix = 'leaflet-rrose'
                const container = (this._container = L.DomUtil.create(
                    'div',
                    prefix + ' ' + this.options.className + ' leaflet-zoom-animated',
                ))
                let closeButton
                let wrapper

                if (this.options.closeButton) {
                    closeButton = this._closeButton = L.DomUtil.create('a', prefix + '-close-button', container)
                    closeButton.href = '#close'
                    closeButton.innerHTML = '&#215;'

                    L.DomEvent.on(closeButton, 'click', this._onCloseButtonClick, this)
                }

                // Set the pixel distances from the map edges at which popups are too close and need to be re-oriented.
                const xBound = 200
                const yBound = 200
                // Determine the alternate direction to pop up; north mimics Leaflet's default behavior, so we initialize to that.
                this.options.position = 'n'
                // Then see if the point is too far north...
                const yDiff = yBound - this._map.latLngToContainerPoint(this._latlng).y
                if (yDiff > 0) {
                    this.options.position = 's'
                }
                // or too far east...
                let xDiff = this._map.latLngToContainerPoint(this._latlng).x - (this._map.getSize().x - xBound)
                if (xDiff > 0) {
                    this.options.position += 'w'
                } else {
                    // or too far west.
                    xDiff = xBound - this._map.latLngToContainerPoint(this._latlng).x
                    if (xDiff > 0) {
                        this.options.position += 'e'
                    }
                }

                // Create the necessary DOM elements in the correct order. Pure 'n' and 's' conditions need only one class for styling, others need two.
                if (/s/.test(this.options.position)) {
                    if (this.options.position === 's') {
                        this._tipContainer = L.DomUtil.create('div', prefix + '-tip-container', container)
                        wrapper = this._wrapper = L.DomUtil.create('div', prefix + '-content-wrapper', container)
                    } else {
                        this._tipContainer = L.DomUtil.create(
                            'div',
                            prefix + '-tip-container' + ' ' + prefix + '-tip-container-' + this.options.position,
                            container,
                        )
                        wrapper = this._wrapper = L.DomUtil.create(
                            'div',
                            prefix + '-content-wrapper' + ' ' + prefix + '-content-wrapper-' + this.options.position,
                            container,
                        )
                    }
                    this._tip = L.DomUtil.create(
                        'div',
                        prefix + '-tip' + ' ' + prefix + '-tip-' + this.options.position,
                        this._tipContainer,
                    )
                    L.DomEvent.disableClickPropagation(wrapper)
                    this._contentNode = L.DomUtil.create('div', prefix + '-content', wrapper)
                    L.DomEvent.on(this._contentNode, 'mousewheel', L.DomEvent.stopPropagation)
                    if (closeButton) closeButton.style.top = '20px'
                } else {
                    if (this.options.position === 'n') {
                        wrapper = this._wrapper = L.DomUtil.create('div', prefix + '-content-wrapper', container)
                        this._tipContainer = L.DomUtil.create('div', prefix + '-tip-container', container)
                    } else {
                        wrapper = this._wrapper = L.DomUtil.create(
                            'div',
                            prefix + '-content-wrapper' + ' ' + prefix + '-content-wrapper-' + this.options.position,
                            container,
                        )
                        this._tipContainer = L.DomUtil.create(
                            'div',
                            prefix + '-tip-container' + ' ' + prefix + '-tip-container-' + this.options.position,
                            container,
                        )
                    }
                    L.DomEvent.disableClickPropagation(wrapper)
                    this._contentNode = L.DomUtil.create('div', prefix + '-content', wrapper)
                    L.DomEvent.on(this._contentNode, 'mousewheel', L.DomEvent.stopPropagation)
                    this._tip = L.DomUtil.create(
                        'div',
                        prefix + '-tip' + ' ' + prefix + '-tip-' + this.options.position,
                        this._tipContainer,
                    )
                }
            },

            _updatePosition: function () {
                const pos = this._map.latLngToLayerPoint(this._latlng)
                const is3d = L.Browser.any3d
                const offset = new L.Point(...this.options.offset)

                L.DomUtil.setPosition(this._container, pos)

                if (/s/.test(this.options.position)) {
                    this._containerBottom = -this._container.offsetHeight + offset.y - (is3d ? 0 : pos.y)
                } else {
                    this._containerBottom = -offset.y - (is3d ? 0 : pos.y)
                }

                if (/e/.test(this.options.position)) {
                    this._containerLeft = offset.x + (is3d ? 0 : pos.x)
                } else if (/w/.test(this.options.position)) {
                    this._containerLeft = -Math.round(this._containerWidth) + offset.x + (is3d ? 0 : pos.x)
                } else {
                    this._containerLeft = -Math.round(this._containerWidth / 2) + offset.x + (is3d ? 0 : pos.x)
                }

                this._container.style.bottom = this._containerBottom + 'px'
                this._container.style.left = this._containerLeft + 'px'
            },
        })
        document.head.appendChild(document.createElement('style')).appendChild(
            document.createTextNode(`/* Rrose layout */

.leaflet-rrose {
position: absolute;
text-align: center;
}

.leaflet-rrose-content-wrapper {
padding: 1px;
text-align: left;
}

.leaflet-rrose-content {
margin: 14px 20px;
}

.leaflet-rrose-tip-container {
margin: 0 auto;
width: 40px;
height: 20px;
position: relative;
overflow: hidden;
}

.leaflet-rrose-tip-container-se, .leaflet-rrose-tip-container-ne {
margin-left: 0;
}

.leaflet-rrose-tip-container-sw, .leaflet-rrose-tip-container-nw {
margin-right: 0;
}

.leaflet-rrose-tip {
width: 15px;
height: 15px;
padding: 1px;

-moz-transform: rotate(45deg);
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
-o-transform: rotate(45deg);
transform: rotate(45deg);
}

.leaflet-rrose-tip-n {
margin: -8px auto 0;
}

.leaflet-rrose-tip-s {
margin: 11px auto 0;
}

.leaflet-rrose-tip-se {
margin: 11px 11px 11px -8px; overflow: hidden;
}

.leaflet-rrose-tip-sw {
margin: 11px 11px 11px 32px; overflow: hidden;
}

.leaflet-rrose-tip-ne {
margin: -8px 11px 11px -8px; overflow: hidden;
}

.leaflet-rrose-tip-nw {
margin: -8px 11px 11px 32px; overflow: hidden;
}

a.leaflet-rrose-close-button {
position: absolute;
top: 0;
right: 0;
padding: 4px 5px 0 0;
text-align: center;
width: 18px;
height: 14px;
font: 16px/14px Tahoma, Verdana, sans-serif;
color: #c3c3c3;
text-decoration: none;
font-weight: bold;
}

a.leaflet-rrose-close-button:hover {
color: #999;
}

.leaflet-rrose-content p {
margin: 18px 0;
}

.leaflet-rrose-scrolled {
overflow: auto;
border-bottom: 1px solid #ddd;
border-top: 1px solid #ddd;
}

/* Visual appearance */

.leaflet-rrose-content-wrapper, .leaflet-rrose-tip {
background: white;

box-shadow: 0 3px 10px #888;
-moz-box-shadow: 0 3px 10px #888;
-webkit-box-shadow: 0 3px 14px #999;
}

.leaflet-rrose-content-wrapper {
-moz-border-radius:    20px;
-webkit-border-radius: 20px;
border-radius:         20px;
}

.leaflet-rrose-content-wrapper-se {
-moz-border-radius:    0 20px 20px 20px;
-webkit-border-radius: 0 20px 20px 20px;
border-radius:         0 20px 20px 20px;
}

.leaflet-rrose-content-wrapper-sw {
-moz-border-radius:    20px 0 20px 20px;
-webkit-border-radius: 20px 0 20px 20px;
border-radius:         20px 0 20px 20px;
}

.leaflet-rrose-content-wrapper-nw, .leaflet-rrose-content-wrapper-w {
-moz-border-radius:    20px 20px 0 20px;
-webkit-border-radius: 20px 20px 0 20px;
border-radius:         20px 20px 0 20px;
}

.leaflet-rrose-content-wrapper-ne, .leaflet-rrose-content-wrapper-e {
-moz-border-radius:    20px 20px 20px 0;
-webkit-border-radius: 20px 20px 20px 0;
border-radius:         20px 20px 20px 0;
}

.leaflet-rrose-content {
font: 12px/1.4 "Helvetica Neue", Arial, Helvetica, sans-serif;
}`),
        )
    }
})()