eBird Alerts Map

Adds a Google Map with eBird alert locations as markers.

当前为 2024-04-13 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         eBird Alerts Map
// @namespace    http://tampermonkey.net/
// @version      2024-04-13.1
// @description  Adds a Google Map with eBird alert locations as markers.
// @author       Ruslan Balagansky
// @license      MIT
// @match        https://ebird.org/alert/needs/*
// @match        https://ebird.org/alert/rba/*
// @match        https://ebird.org/alert/summary?sid=*
// @require      http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js
// @icon         https://www.google.com/s2/favicons?sz=64&domain=tampermonkey.net
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function() {
    'use strict';

    const mapDivId = 'userscript-map';

    // load api key from storage or use default
    const defaultApiKey = "AIzaSyCNhkdcs7rdwXoaSpqDzNLBnA-4Tu_7v-4"  // restricted to ebird.org
    var apiKey = GM_getValue("apiKey", defaultApiKey);

    // handle case when stored value is an empty string
    if (!apiKey) {
        apiKey = defaultApiKey;
    }

    // allow user to set a custom key via script's menu (via Tampermonkey extension icon)
    function promptForApiKey() {
        apiKey = prompt("Enter a Google Maps API key or accept the author-provided one: ", defaultApiKey);
        GM_setValue("apiKey", apiKey);
    }

    GM_registerMenuCommand("Change Google Maps API Key", promptForApiKey);

    // initializes the map (called by legacy API callback)
    function initMap() {
        // Collect locations data
        var locations = {};
        const mapRegex = /Map: (.+), (.+)/;
        const observations = document.getElementsByClassName("Observation");
        for (const obs of observations) {
            const species = obs.getElementsByClassName("Observation-species")[0];
            const specRef = species.getElementsByTagName("a")[0];
            const speciesCode = specRef.getAttribute("data-species-code");
            const fourLetterSpecies = speciesCode.slice(0, 2) + speciesCode.slice(3, 5)

            const meta = obs.getElementsByClassName("Observation-meta")[0];
            let coords;
            let key;
            for (const a of meta.getElementsByTagName("a")) {
                const title = a.getAttribute("title");
                const mapMatch = title.match(mapRegex);
                if (mapMatch) {
                    key = title;
                    coords = { lat: Number(mapMatch[1]), lng: Number(mapMatch[2]) };
                }
            }

            if (!(key in locations)) {
                locations[key] = {
                    labels:new Set(),
                    html:[]
                };
            }
            var loc = locations[key];
            loc.coords = coords;
            loc.labels.add(fourLetterSpecies);
            loc.html.push(obs.innerHTML);
        }

        // compute the center based on alert locations
        var mapCenter = { lat: 32.92, lng: -116.85 }; // default to San Diego

        let minLat = Infinity, minLng = Infinity, maxLat = -Infinity, maxLng = -Infinity;
        for (const loc of Object.values(locations)) {
            const lat = loc.coords.lat;
            const lng = loc.coords.lng;
            minLat = Math.min(minLat, lat);
            maxLat = Math.max(maxLat, lat);
            minLng = Math.min(minLng, lng);
            maxLng = Math.max(maxLng, lng);
        }

        mapCenter = { lat: (maxLat + minLat) / 2, lng: (maxLng + minLng) / 2 };

        // Create the map object
        var mapOptions = {
            center: mapCenter,
            zoom: 9 // Set the initial zoom level
        };

        var map = new google.maps.Map(document.getElementById(mapDivId), mapOptions);

        // Create an InfoWindow for markers
        const infoWindow = new google.maps.InfoWindow();

        // Create location markers
        for (const location of Object.values(locations)) {
            var label = location.labels.values().next().value;
            if (location.labels.size > 1) {
                label += "+" + (location.labels.size - 1).toString();
            }
            const marker = new google.maps.Marker({
                map: map,
                position: location.coords,
                label: label
            });

            marker.addListener("click", () => {
                infoWindow.close();
                infoWindow.setContent(location.html.join());
                infoWindow.open(marker.getMap(), marker);
            });
        }
    }

    // adds map div and sets up the callback to initialize the map
    function embedGoogleMap() {
        // Create a div element to hold the map
        var mapDiv = document.createElement('div');
        mapDiv.id = mapDivId; // Set the ID for the div

        // Set the size and position of the map div
        mapDiv.style.width = '800px';
        mapDiv.style.height = '400px';
        mapDiv.style.margin = 'auto';

        // add map div above the list section
        var pageSectionInner = document.getElementsByClassName("Page-section--primaryLight")[0];
        pageSectionInner.appendChild(mapDiv);

        // Load the Google Maps JavaScript API
        var script = document.createElement('script');
        script.src = 'https://maps.googleapis.com/maps/api/js?key=' + apiKey;
        script.defer = true;
        document.head.appendChild(script);

        // Call the initMap function once the API script is loaded
        script.onload = function() {
            initMap();
        };
    }

    // do it!
    embedGoogleMap();
})();