diff --git a/extensions/SamuelLouf/weather.js b/extensions/SamuelLouf/weather.js new file mode 100644 index 0000000000..99e492b2be --- /dev/null +++ b/extensions/SamuelLouf/weather.js @@ -0,0 +1,415 @@ +// Name: Weather +// ID: samuelloufweather +// Description: Gets the weather anywhere. +// By: SamuelLouf +// License: MIT + +(function (Scratch) { + "use strict"; + + const icon = + "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIyOTkuNDk4NjEiIGhlaWdodD0iMjk5LjM4MzQiIHZpZXdCb3g9IjAsMCwyOTkuNDk4NjEsMjk5LjM4MzQiPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKC05MC4yNTA2OSwtMzAuMzA4MykiPjxnIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIj48cGF0aCBkPSJNMzg5Ljc0OTMxLDE3OS45OTk5OWMwLDgyLjY3MjQ1IC02Ny4wNDUwNSwxNDkuNjkxNzEgLTE0OS43NDkzLDE0OS42OTE3MWMtODIuNzA0MjUsMCAtMTQ5Ljc0OTMsLTY3LjAxOTI3IC0xNDkuNzQ5MywtMTQ5LjY5MTcxYzAsLTgyLjY3MjQ1IDY3LjA0NTA2LC0xNDkuNjkxNjkgMTQ5Ljc0OTMxLC0xNDkuNjkxNjljODIuNzA0MjUsMCAxNDkuNzQ5MzEsNjcuMDE5MjYgMTQ5Ljc0OTMxLDE0OS42OTE3MXoiIGZpbGw9IiNmZjgwMDAiIHN0cm9rZS13aWR0aD0iMCIvPjxwYXRoIGQ9Ik0zMDYuNDk4MDcsMTgzLjY4MDU4YzAsMzYuMDgyNDUgLTI5LjczNzM5LDY1LjMzMzA1IC02Ni40MjAzMSw2NS4zMzMwNWMtMzYuNjgyOTIsMCAtNjYuNDIwMzEsLTI5LjI1MDYxIC02Ni40MjAzMSwtNjUuMzMzMDVjMCwtMzYuMDgyNDUgMjkuNzM3MzksLTY1LjMzMzA0IDY2LjQyMDMxLC02NS4zMzMwNGMzNi42ODI5MiwwIDY2LjQyMDMxLDI5LjI1MDYgNjYuNDIwMzEsNjUuMzMzMDV6IiBmaWxsPSIjZmZmZjAwIiBzdHJva2Utd2lkdGg9IjkiLz48cGF0aCBkPSJNMjM5LjM3MTM4LDI1NC42MDY4OWw0MC4yMzIyLC0xNC4wNDQ1M2wtNi41NTU0NSw1My4xMDQ5MnoiIGZpbGw9IiNmZmZmMDAiIHN0cm9rZS13aWR0aD0iOSIvPjxwYXRoIGQ9Ik0yODEuOTgyMzksMjM3Ljk3MDk4bDI2LjU2MTA3LC0zNi4zMDg5NmwxNC45Mjc3NiwzOC4yNjE5N3oiIGZpbGw9IiNmZmZmMDAiIHN0cm9rZS13aWR0aD0iOSIvPjxwYXRoIGQ9Ik0zMDIuNjY4NzgsMTUzLjAxMzUzbDQ5LjYwNjcsMjEuMTA5ODJsLTQzLjY2MjgyLDI0LjU5MDg0eiIgZmlsbD0iI2ZmZmYwMCIgc3Ryb2tlLXdpZHRoPSI5Ii8+PHBhdGggZD0iTTI2Ni43ODU3NSwxMjAuMzAwNjRsNjEuMzI0ODIsLTMzLjE4NDEybC0yNC41MjMyMyw2My4yNjA2M3oiIGZpbGw9IiNmZmZmMDAiIHN0cm9rZS13aWR0aD0iOSIvPjxwYXRoIGQ9Ik0xODEuODY3OTYsMTQ1LjEyNTE5bC0yMS40ODMyMSwtNzIuMjQ0NTFsNjEuNDIwOTIsNDQuODIzMDd6IiBmaWxsPSIjZmZmZjAwIiBzdHJva2Utd2lkdGg9IjkiLz48cGF0aCBkPSJNMjIxLjA1NDEzLDExNS44MDg3N2wyNi41NjEwNiwtMjkuMjc4MDhsMjAuMDA1NjEsMzAuNDQ5OXoiIGZpbGw9IiNmZmZmMDAiIHN0cm9rZS13aWR0aD0iOSIvPjxwYXRoIGQ9Ik0xMjYuMTk5OTksMjM4LjAzODk5bDQyLjk2NjQzLC00Ny42MzY0N2wyMC43ODY4Miw0My4zMzk4MnoiIGZpbGw9IiNmZmZmMDAiIHN0cm9rZS13aWR0aD0iOSIvPjxwYXRoIGQ9Ik0xODQuNzg2MTIsMjkzLjMzMDUybDUuMzgyNjcsLTU4LjQxMTUzbDQ1Ljg3MTI1LDE3LjM5ODEzeiIgZmlsbD0iI2ZmZmYwMCIgc3Ryb2tlLXdpZHRoPSI5Ii8+PHBhdGggZD0iTTE2OS43MDEzMywxOTEuNzg2NjZsLTI2LjE3MDQ2LC0zMi43OTM1MmwzMi44OTU1NCwtMTEuNzM1MzJ6IiBmaWxsPSIjZmZmZjAwIiBzdHJva2Utd2lkdGg9IjkiLz48L2c+PC9nPjwvc3ZnPjwhLS1yb3RhdGlvbkNlbnRlcjoxNDkuNzQ5MzA1OjE0OS42OTE3LS0+"; + + /** + * Get Current Weather + * @param {{ latitude: any; longitude: any; }} coordinates + */ + async function getCurrentWeather(coordinates) { + var fetched_json = await Scratch.fetch( + `https://api.open-meteo.com/v1/forecast?latitude=${coordinates.latitude}&longitude=${coordinates.longitude}¤t=temperature_2m,relative_humidity_2m,apparent_temperature,precipitation,rain,showers,snowfall,weather_code,cloud_cover,pressure_msl,surface_pressure,wind_speed_10m,wind_direction_10m,wind_gusts_10m` + ) + .then((r) => r.text()) + .catch(() => ""); + if (!fetched_json) return ""; + fetched_json = JSON.parse(fetched_json); + // @ts-ignore + return fetched_json.current; + } + + /** + * Is it day ? + * @param {{ latitude: any; longitude: any; }} coordinates + */ + async function isDay(coordinates) { + var fetched_json = await Scratch.fetch( + `https://api.open-meteo.com/v1/forecast?latitude=${coordinates.latitude}&longitude=${coordinates.longitude}¤t=is_day` + ) + .then((r) => r.text()) + .catch(() => ""); + if (!fetched_json) return ""; + fetched_json = JSON.parse(fetched_json); + // @ts-ignore + return fetched_json.current.is_day == 1; + } + + /** + * Slice + * @param {String} text + * @param {*} from + * @param {*} to + * @returns {String} + */ + const lettersOf = (text, from, to) => text.substring(from || 0, to || 0); + + /** + * Get an item from a split string + * @param {Number} no + * @param {*} from + * @param {String} splitby + * @returns {String} + */ + const itemOfFromString = (no, from, splitby) => + Scratch.Cast.toString(from).split(splitby)[no] || ""; + + /** + * Compare two numbers + * @param {Number} a + * @param {Number} b + * @param {String} comparison + * @param {Number} margin + */ + function compare(a, b, comparison, margin = 0) { + if (comparison == "<") return a < b; + if (comparison == ">") return a > b; + if (comparison == "=") return a == b; + return Math.abs(a - b) <= margin; + } + + /** + * Get latitude and longitude from adress + * @param {string} adress + */ + async function getCoordinatesFromAdress(adress) { + var response = await Scratch.fetch( + `https://photon.komoot.io/api/?q=${encodeURIComponent(adress)}` + ) + .then((r) => r.text()) + .catch(() => ""); + if (!response) return ""; + response = JSON.parse(response); + // @ts-ignore + return response.features[0].geometry.coordinates; + } + + const meteo_now_menu = [ + "temperature (C)", + "temperature (F)", + "apparent temperature (C)", + "apparent temperature (F)", + "weather code", + "wind direction (°)", + "wind speed (km/h)", + "wind speed (mph)", + "wind gusts (km/h)", + "wind gusts (mph)", + "precipitation (mm)", + "precipitation (inches)", + "rain (mm)", + "rain (inches)", + "showers (mm)", + "showers (inches)", + "snowfall (cm)", + "snowfall (inches)", + "surface pressure (hPa)", + { + text: "sea level pressure (hPa)", + value: "pressure_msl", + }, + "cloud cover (%)", + { + text: "relative humidity (%)", + value: "relative_humidity_2m", + }, + ]; + + class Weather { + getInfo() { + return { + id: "samuelloufweather", + name: Scratch.translate("Weather"), + color1: "#ff8800", + menuIconURI: icon, + blocks: [ + { + opcode: "is_current_weather_at", + blockType: Scratch.BlockType.BOOLEAN, + text: Scratch.translate( + "is current [METEO] at latitude [latitude] longitude [longitude] [OPERATORS] [value] degrees" + ), + arguments: { + METEO: { + type: Scratch.ArgumentType.STRING, + menu: "meteo_now_no_json_menu", + }, + latitude: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 42.3592593, + }, + longitude: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: -71.0933625, + }, + OPERATORS: { + type: Scratch.ArgumentType.STRING, + menu: "operators_menu", + defaultValue: ">", + }, + value: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 20, + }, + }, + }, + { + opcode: "get_current_weather_at", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate( + "current [METEO] at latitude [latitude] longitude [longitude]" + ), + arguments: { + METEO: { + type: Scratch.ArgumentType.STRING, + menu: "meteo_now_menu", + }, + latitude: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 42.3592593, + }, + longitude: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: -71.0933625, + }, + }, + }, + { + opcode: "is_day_at", + blockType: Scratch.BlockType.BOOLEAN, + text: Scratch.translate( + "daytime at latitude [latitude] longitude [longitude]?" + ), + arguments: { + latitude: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 42.3592593, + }, + longitude: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: -71.0933625, + }, + }, + }, + "---", + "---", + { + opcode: "is_current_weather_in", + blockType: Scratch.BlockType.BOOLEAN, + text: Scratch.translate( + "is current [METEO] in [place] [OPERATORS] [value] degrees" + ), + arguments: { + METEO: { + type: Scratch.ArgumentType.STRING, + menu: "meteo_now_no_json_menu", + }, + place: { + type: Scratch.ArgumentType.STRING, + defaultValue: "Paris, France", + }, + OPERATORS: { + type: Scratch.ArgumentType.STRING, + menu: "operators_menu", + defaultValue: ">", + }, + value: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 20, + }, + }, + }, + { + opcode: "get_current_weather_in", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("current [METEO] in [place]"), + arguments: { + METEO: { + type: Scratch.ArgumentType.STRING, + menu: "meteo_now_menu", + }, + place: { + type: Scratch.ArgumentType.STRING, + defaultValue: "Paris, France", + }, + }, + }, + { + opcode: "is_day_in", + blockType: Scratch.BlockType.BOOLEAN, + text: Scratch.translate("daytime in [place]?"), + arguments: { + place: { + type: Scratch.ArgumentType.STRING, + defaultValue: "Paris, France", + }, + }, + }, + ], + menus: { + meteo_now_menu: { + acceptReporters: true, + items: meteo_now_menu.concat("all (JSON)"), + }, + meteo_now_no_json_menu: { + acceptReporters: true, + items: meteo_now_menu, + }, + operators_menu: { + acceptReporters: true, + items: ["<", ">", "=", "≈"], + }, + }, + }; + } + + /** + * @param {{ [x: string]: any; temperature: number; windspeed: number; }} weather + * @param {{ METEO: string; }} args + */ + get_current_weather(weather, args) { + if (args.METEO.includes("temperature")) { + if (args.METEO.includes("(C)")) { + return weather[ + args.METEO.includes("apparent") + ? "apparent_temperature" + : "temperature_2m" + ]; + } else { + var temperature = + (weather[ + args.METEO.includes("apparent") + ? "apparent_temperature" + : "temperature_2m" + ] * + 9) / + 5 + + 32; + return Scratch.Cast.toNumber( + Scratch.Cast.toString(itemOfFromString(0, temperature, ".")) + + "." + + lettersOf(itemOfFromString(1, temperature, "."), 0, 1) + ); + } + } else if (args.METEO == "all (JSON)") { + return JSON.stringify(weather); + } else if (args.METEO.includes("wind")) { + if (args.METEO.includes("(km/h)") || args.METEO.includes("(°)")) { + return weather[ + args.METEO.split(" (")[0].replaceAll(" ", "_") + "_10m" + ]; + } else { + var wind_speed = + weather[args.METEO.split(" (")[0].replaceAll(" ", "_") + "_10m"] / + 1.609; + return Scratch.Cast.toNumber( + Scratch.Cast.toString(itemOfFromString(0, wind_speed, ".")) + + "." + + lettersOf(itemOfFromString(1, wind_speed, "."), 0, 1) + ); + } + } else if ( + ["Precipitation", "Rain", "Showers", "Snowfall"].includes( + args.METEO.split(" ")[0] + ) + ) { + if (args.METEO.includes("(cm)") || args.METEO.includes("(mm)")) { + return weather[args.METEO.split(" ")[0].toLocaleLowerCase()]; + } else { + return ( + Math.round( + ((weather[args.METEO.split(" ")[0].toLocaleLowerCase()] * + (args.METEO.split(" ")[0] == "Snowfall" ? 10 : 1)) / + 25.4) * + 10 ** 4 + ) / + 10 ** 4 + ); + } + } else { + return weather[ + args.METEO.toLocaleLowerCase().split(" (")[0].replaceAll(" ", "_") + ]; + } + } + + /** + * @param {{ OPERATORS: string; value: any; METEO: string | string[]; }} args + */ + async is_current_weather_at(args) { + var value = Scratch.Cast.toNumber(args.value); + var response = await this.get_current_weather_at(args); + response = Scratch.Cast.toNumber(response); + var margin = 10; + return compare(response, value, args.OPERATORS, margin); + } + + /** + * @param {{ OPERATORS?: string; value?: any; METEO?: any; latitude?: any; longitude?: any; }} args + */ + async get_current_weather_at(args) { + var current_weather = await getCurrentWeather({ + latitude: args.latitude, + longitude: args.longitude, + }); + return this.get_current_weather(current_weather, { METEO: args.METEO }); + } + + /** + * @param {{ latitude?: any; longitude?: any; }} args + */ + async is_day_at(args) { + return await isDay({ + latitude: args.latitude, + longitude: args.longitude, + }); + } + + // --- + // --- + + /** + * @param {{ OPERATORS: string; value: any; METEO: string | string[]; }} args + */ + async is_current_weather_in(args) { + var response = await this.get_current_weather_in(args); + var value = Scratch.Cast.toNumber(args.value); + response = Scratch.Cast.toNumber(response); + var margin = 10; + return compare(response, value, args.OPERATORS, margin); + } + + /** + * @param {{ OPERATORS?: string; value?: any; METEO: any; place?: any; }} args + */ + async get_current_weather_in(args) { + var geolocation = await getCoordinatesFromAdress(args.place); + if (!geolocation) return ""; + var current_weather = await getCurrentWeather({ + latitude: geolocation[1], + longitude: geolocation[0], + }); + return this.get_current_weather(current_weather, { METEO: args.METEO }); + } + + /** + * @param {{ place?: any; }} args + */ + async is_day_in(args) { + var geolocation = await getCoordinatesFromAdress(args.place); + if (!geolocation) return ""; + return await isDay({ + latitude: geolocation[1], + longitude: geolocation[0], + }); + } + } + Scratch.extensions.register(new Weather()); +})(Scratch); diff --git a/extensions/extensions.json b/extensions/extensions.json index a4fac91813..7c369a0da8 100644 --- a/extensions/extensions.json +++ b/extensions/extensions.json @@ -103,5 +103,6 @@ "itchio", "gamejolt", "obviousAlexC/newgroundsIO", + "SamuelLouf/weather", "Lily/McUtils" // McUtils should always be the last item. ] diff --git a/images/README.md b/images/README.md index b2dde23dc9..475680b7fa 100644 --- a/images/README.md +++ b/images/README.md @@ -328,3 +328,6 @@ All images in this folder are licensed under the [GNU General Public License ver ## NishiOwO/dectalk.png - Created by [@Dogo6647](https://github.com/Dogo6647) - Used Arimo from Google Fonts. + +## SamuelLouf/weather.png + - Created by [@horizontal-shading](https://github.com/horizontal-shading) for [@samuellouf](https://github.com/samuellouf)'s "Weather" extension diff --git a/images/SamuelLouf/weather.svg b/images/SamuelLouf/weather.svg new file mode 100644 index 0000000000..a8a9944568 --- /dev/null +++ b/images/SamuelLouf/weather.svg @@ -0,0 +1 @@ + \ No newline at end of file