diff --git a/extensions/SamuelLouf/Geolocation.js b/extensions/SamuelLouf/Geolocation.js new file mode 100644 index 0000000000..48de91a9d6 --- /dev/null +++ b/extensions/SamuelLouf/Geolocation.js @@ -0,0 +1,283 @@ +// Name: Geolocation +// ID: samuelloufgeolocation +// Description: Get the user's geolocation. +// By: SamuelLouf +// License: MPL-2.0 + +(function (Scratch) { + "use strict"; + + function getGeolocation( + options = { enableHighAccuracy: true, timeout: 5000, maximumAge: 0 } + ) { + return new Promise((resolve) => { + function success(pos) { + resolve({ + success: true, + latitude: pos.coords.latitude, + longitude: pos.coords.longitude, + accuracy: pos.coords.accuracy, + }); + } + + function error(err) { + resolve({ + success: false, + error: { + code: err.code, + message: err.message, + }, + }); + } + + navigator.geolocation.getCurrentPosition(success, error, options); + }); + } + + const icon = + "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIxMTAuODAyNSIgaGVpZ2h0PSIxMTAuNDAyNDkiIHZpZXdCb3g9IjAsMCwxMTAuODAyNSwxMTAuNDAyNDkiPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xODQuNTk4NzUsLTEyNC43OTg3NSkiPjxnIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2lzUGFpbnRpbmdMYXllciZxdW90Ozp0cnVlfSIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgc3Ryb2tlLWRhc2hhcnJheT0iIiBzdHJva2UtZGFzaG9mZnNldD0iMCIgc3R5bGU9Im1peC1ibGVuZC1tb2RlOiBub3JtYWwiPjxwYXRoIGQ9Ik0xODQuNTk4NzUsMjM1LjIwMTI1di0xMTAuNDAyNDloMTEwLjgwMjV2MTEwLjQwMjQ5eiIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJub256ZXJvIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMCIvPjxwYXRoIGQ9Ik0yNDEuNzc3MTQsMjE2LjQwNzcyYy0wLjU0NTUyLDAuNDA5NjQgLTEuMjg5MjEsMC40MzM2MyAtMS44NiwwLjA2Yy02LjE0MzY2LC0zLjg2ODY0IC0xMS41OTg1OSwtOC43MzU1IC0xNi4xNCwtMTQuNGMtNS41NTkxLC02Ljc1NTkyIC05LjI5NzYsLTE0LjgyMTU4IC0xMC44NiwtMjMuNDNjLTEuMjksLTcuOTkgLTAuMDYsLTE1LjY3IDMuOTQsLTIxLjkyYzEuNjE0OTMsLTIuNTM4ODEgMy42NDI4NywtNC43ODk4MyA2LC02LjY2YzUuMTU3NjksLTQuMzA2MjUgMTEuNjQxNSwtNi43MDQxMyAxOC4zNiwtNi43OWM2LjUwNTg5LDAuMTA0OTYgMTIuNzM1OTYsMi42NDU1MSAxNy40Niw3LjEyYzEuODE0MzUsMS42NjMyMiAzLjM4MzM1LDMuNTc1NjQgNC42Niw1LjY4YzQuMjcsNyA1LjE5LDE2IDMuMzEsMjUuMTJjLTMuMTgyMzQsMTQuNjIxNzcgLTEyLjE1NTQ4LDI3LjMyOTIyIC0yNC44NywzNS4yMnpNMjQwLjAwNzE0LDE1Ny40Nzc3MWM3LjU3NzM1LDAgMTMuNzIsNi4xNDI2NSAxMy43MiwxMy43MmMwLDcuNTc3MzUgLTYuMTQyNjUsMTMuNzIgLTEzLjcyLDEzLjcyYy03LjU3NzM1LDAgLTEzLjcyLC02LjE0MjY1IC0xMy43MiwtMTMuNzJjMCwtNy41NzczNSA2LjE0MjY1LC0xMy43MiAxMy43MiwtMTMuNzJ6IiBmaWxsPSIjMDM2ZTE1IiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0iIzAwYTExYiIgc3Ryb2tlLXdpZHRoPSIxLjUiLz48L2c+PC9nPjwvc3ZnPjwhLS1yb3RhdGlvbkNlbnRlcjo1NS40MDEyNTAwMDAwMDAwMDU6NTUuMjAxMjQ1LS0+"; + + class Geolocation { + constructor() { + this.options = { + enableHighAccuracy: true, + timeout: 10000, + maximumAge: 0, + }; + + this.isWatching = false; + this.watcherID = null; + } + + getInfo() { + return { + id: "samuelloufgeolocation", + name: Scratch.translate("Geolocation"), + color1: "#036e15", + color2: "#00A11B", + menuIconURI: icon, + blocks: [ + { + opcode: "isSupported", + blockType: Scratch.BlockType.BOOLEAN, + text: Scratch.translate("is geolocation supported by this device?"), + }, + { + opcode: "isAllowed", + blockType: Scratch.BlockType.BOOLEAN, + text: Scratch.translate("is geolocation allowed?"), + }, + "---", + { + opcode: "onUserMove", + blockType: Scratch.BlockType.EVENT, + text: Scratch.translate("when the user's position changes"), + isEdgeActivated: false, + }, + { + opcode: "changePositionWatching", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("[VALUE] watching the user's position"), + arguments: { + VALUE: { + type: Scratch.ArgumentType.STRING, + menu: "start_stop", + }, + }, + }, + { + opcode: "isWatchingPos", + blockType: Scratch.BlockType.BOOLEAN, + text: Scratch.translate( + "is TurboWarp watching the users position?" + ), + }, + "---", + { + opcode: "getCurrent", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("get [WHAT]"), + arguments: { + WHAT: { + type: Scratch.ArgumentType.STRING, + menu: "coordinates", + }, + }, + }, + "---", + { + blockType: Scratch.BlockType.LABEL, + text: Scratch.translate("Geolocation Options"), + }, + { + opcode: "setTimeoutTo", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("set timeout to [SECONDS] seconds"), + arguments: { + SECONDS: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 10, + }, + }, + }, + { + opcode: "addToTimeout", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("add [SECONDS] seconds to timeout"), + arguments: { + SECONDS: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + }, + }, + { + opcode: "getTimeout", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("get timeout"), + }, + { + opcode: "setAccuracy", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("set accuracy to [ACCURACY]"), + arguments: { + ACCURACY: { + type: Scratch.ArgumentType.STRING, + menu: "accuracy", + }, + }, + }, + { + opcode: "isHighAccuracy", + blockType: Scratch.BlockType.BOOLEAN, + text: Scratch.translate("is the accuracy high?"), + }, + ], + menus: { + coordinates: { + acceptReporters: true, + items: [ + { + text: Scratch.translate("latitude"), + value: "latitude", + }, + { + text: Scratch.translate("longitude"), + value: "longitude", + }, + { + text: Scratch.translate("accuracy"), + value: "accuracy", + }, + ], + }, + accuracy: { + acceptReporters: true, + items: [ + { + text: Scratch.translate("high"), + value: "high", + }, + { + text: Scratch.translate("low"), + value: "low", + }, + ], + }, + start_stop: { + acceptReporters: true, + items: [ + { + text: Scratch.translate("start"), + value: "start", + }, + { + text: Scratch.translate("stop"), + value: "stop", + }, + ], + }, + }, + }; + } + + async getCurrent(args, util) { + if (!(await this.isAllowed())) return ""; + var coordinates = + util.thread._coordinates || (await getGeolocation(this.options)); + if (coordinates.success == true) { + return coordinates[args.WHAT]; + } else { + return ""; + } + } + + async changePositionWatching(args) { + if ( + (args.VALUE == "start" && this.isWatching) || + (args.VALUE == "stop" && !this.isWatching) + ) + return ""; + if (!(await this.isAllowed())) return ""; + + this.isWatching = args.VALUE == "start"; + + if (args.VALUE == "start") { + this.watcherID = navigator.geolocation.watchPosition( + (pos) => { + var threads = Scratch.vm.runtime.startHats( + "samuelloufgeolocation_onUserMove" + ); + for (var thread of threads) { + var coords = pos.toJSON().coords; + coords.success = true; + // @ts-ignore + thread._coordinates = coords; + } + }, + () => {}, + this.options + ); + } else { + navigator.geolocation.clearWatch(this.watcherID); + } + } + + isWatchingPos() { + return this.isWatching; + } + + async isAllowed() { + if (!this.isSupported()) return false; + // @ts-ignore + return await Scratch.canGeolocate(); + } + + isSupported() { + return !!navigator.geolocation; + } + + setTimeoutTo(args) { + this.options.timeout = Number(args.SECONDS) * 1000; + } + + addToTimeout(args) { + this.options.timeout += Number(args.SECONDS) * 1000; + } + + getTimeout() { + return this.options.timeout / 1000; + } + + setAccuracy(args) { + this.options.enableHighAccuracy = args.ACCURACY === "high"; + } + + isHighAccuracy() { + return this.options.enableHighAccuracy; + } + } + // @ts-ignore + Scratch.extensions.register(new Geolocation()); + // @ts-ignore +})(Scratch); diff --git a/extensions/extensions.json b/extensions/extensions.json index 1c3bc949d8..c5399708e7 100644 --- a/extensions/extensions.json +++ b/extensions/extensions.json @@ -56,6 +56,7 @@ "true-fantom/math", "true-fantom/regexp", "true-fantom/couplers", + "SamuelLouf/Geolocation", "DogeisCut/FormatNumbers", "Lily/AllMenus", "Lily/HackedBlocks", diff --git a/images/SamuelLouf/Geolocation.svg b/images/SamuelLouf/Geolocation.svg new file mode 100644 index 0000000000..c39ecaa0c2 --- /dev/null +++ b/images/SamuelLouf/Geolocation.svg @@ -0,0 +1 @@ + \ No newline at end of file