diff --git a/apps/astrocalc/ChangeLog b/apps/astrocalc/ChangeLog index 95b9dbaf1..156cf17bf 100644 --- a/apps/astrocalc/ChangeLog +++ b/apps/astrocalc/ChangeLog @@ -3,3 +3,4 @@ 0.03: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps 0.04: Compatibility with Bangle.js 2, get location from My Location 0.05: Enable widgets +0.06: Fix azimuth (bug #2651), only show degrees diff --git a/apps/astrocalc/astrocalc-app.js b/apps/astrocalc/astrocalc-app.js index 2e732c37a..5589a5703 100644 --- a/apps/astrocalc/astrocalc-app.js +++ b/apps/astrocalc/astrocalc-app.js @@ -140,14 +140,15 @@ function drawData(title, obj, startX, startY) { function drawMoonPositionPage(gps, title) { const pos = SunCalc.getMoonPosition(new Date(), gps.lat, gps.lon); const moonColor = g.theme.dark ? {r: 1, g: 1, b: 1} : {r: 0, g: 0, b: 0}; + const azimuth = pos.azimuth + Math.PI; // 0 is south, we want 0 to be north const pageData = { - Azimuth: pos.azimuth.toFixed(2), - Altitude: pos.altitude.toFixed(2), + Azimuth: parseInt(azimuth * 180 / Math.PI + 0.5) + '°', + Altitude: parseInt(pos.altitude * 180 / Math.PI + 0.5) + '°', Distance: `${pos.distance.toFixed(0)} km`, - "Parallactic Ang": pos.parallacticAngle.toFixed(2), + "Parallactic Ang": parseInt(pos.parallacticAngle * 180 / Math.PI + 0.5) + '°', }; - const azimuthDegrees = parseInt(pos.azimuth * 180 / Math.PI); + const azimuthDegrees = parseInt(azimuth * 180 / Math.PI + 0.5); drawData(title, pageData, null, g.getHeight()/2 - Object.keys(pageData).length/2*20); drawPoints(); @@ -189,12 +190,14 @@ function drawMoonTimesPage(gps, title) { // Draw the moon rise position const risePos = SunCalc.getMoonPosition(times.rise, gps.lat, gps.lon); - const riseAzimuthDegrees = parseInt(risePos.azimuth * 180 / Math.PI); + const riseAzimuth = risePos.azimuth + Math.PI; // 0 is south, we want 0 to be north + const riseAzimuthDegrees = parseInt(riseAzimuth * 180 / Math.PI); drawPoint(riseAzimuthDegrees, 8, moonColor); // Draw the moon set position const setPos = SunCalc.getMoonPosition(times.set, gps.lat, gps.lon); - const setAzimuthDegrees = parseInt(setPos.azimuth * 180 / Math.PI); + const setAzimuth = setPos.azimuth + Math.PI; // 0 is south, we want 0 to be north + const setAzimuthDegrees = parseInt(setAzimuth * 180 / Math.PI); drawPoint(setAzimuthDegrees, 8, moonColor); Bangle.setUI({mode: "custom", back: () => moonIndexPageMenu(gps)}); @@ -207,16 +210,15 @@ function drawSunShowPage(gps, key, date) { const mins = ("0" + date.getMinutes()).substr(-2); const secs = ("0" + date.getMinutes()).substr(-2); const time = `${hrs}:${mins}:${secs}`; + const azimuth = pos.azimuth + Math.PI; // 0 is south, we want 0 to be north - const azimuth = Number(pos.azimuth.toFixed(2)); - const azimuthDegrees = parseInt(pos.azimuth * 180 / Math.PI); - const altitude = Number(pos.altitude.toFixed(2)); + const azimuthDegrees = parseInt(azimuth * 180 / Math.PI + 0.5) + '°'; + const altitude = parseInt(pos.altitude * 180 / Math.PI + 0.5) + '°'; const pageData = { Time: time, Altitude: altitude, - Azimumth: azimuth, - Degrees: azimuthDegrees + Azimuth: azimuthDegrees, }; drawData(key, pageData, null, g.getHeight()/2 - Object.keys(pageData).length/2*20 + 5); diff --git a/apps/astrocalc/metadata.json b/apps/astrocalc/metadata.json index 1f4abb356..09dc53170 100644 --- a/apps/astrocalc/metadata.json +++ b/apps/astrocalc/metadata.json @@ -1,10 +1,10 @@ { "id": "astrocalc", "name": "Astrocalc", - "version": "0.05", + "version": "0.06", "description": "Calculates interesting information on the sun like sunset and sunrise and moon cycles for the current day based on your location from MyLocation app", "icon": "astrocalc.png", - "tags": "app,sun,moon,cycles,tool", + "tags": "app,sun,moon,cycles,tool,outdoors", "supports": ["BANGLEJS", "BANGLEJS2"], "allow_emulator": true, "dependencies": {"mylocation":"app"}, diff --git a/apps/bblobface/ChangeLog b/apps/bblobface/ChangeLog new file mode 100644 index 000000000..6a29fdb72 --- /dev/null +++ b/apps/bblobface/ChangeLog @@ -0,0 +1 @@ +1.00: Initial release of Bangle Blobs Clock! diff --git a/apps/bblobface/README.md b/apps/bblobface/README.md new file mode 100644 index 000000000..54e07e9f8 --- /dev/null +++ b/apps/bblobface/README.md @@ -0,0 +1,35 @@ +# Bangle Blobs Clock +What if every time you checked the time, you could play a turn of a turn-based puzzle game? +You check the time dozens, maybe hundreds of times per day, and Bangle Blobs Clock wants to add a splash of fun to each of these moments! +Bangle Blobs Clock is a fully featured watch face with a turn-based puzzle game right next to the clock. + +![](screenshot1.png) +![](screenshot2.png) + +## Clock Features +- Hour and minute +- Seconds (only while the screen is unlocked to save power) +- Month, day, and day of week +- Battery percentage. Blue while charging, red when low, green otherwise. +- Respects your 24-hour/12-hour time setting in Locale +- Press the pause button to access your Widgets +- Supports Fast Loading + +## The Game +This is a turn-based puzzle game based on Puyo Puyo, an addictive puzzle game franchise by SEGA. +Blobs arrive in pairs that you can move, rotate, and place. When at least four Blobs of the same color touch, they pop, causing Blobs above them to fall. +If this causes another pop, it's called a chain! Build a massive chain reaction of popping Blobs! +- Drag left and right to move the pair +- Tap the left or right half of the screen to rotate the pair +- Swipe down to place the pair + +## More Info +If you're confused about the functionality of the clock or want a better explanation of how to play the game, I wrote up a user manual here: https://docs.google.com/document/d/1watPzChawBu4iM0lXypreejs3wvf2_8C-x5V2MWJQBc/edit?usp=sharing + +## Special Thanks +I'm Pasta Rhythm, computer scientist and aspiring game developer. I would like to say thank you to the people who inspired me while I was making this app: +- [nxdefiant, who made a Tetris game.](https://github.com/espruino/BangleApps/tree/master/apps/tetris) Bangle Blobs is my first Bangle app and my first time using JavaScript, so this was a daunting project. This Tetris game served as a great example that helped me get started. +- [gfwilliams for Anton Clock](https://github.com/espruino/BangleApps/tree/master/apps/antonclk) and [Targor for Kanagawa Clock.](https://github.com/espruino/BangleApps/tree/master/apps/kanagsec) These were good examples for how to make a watch face for the Bangle.js 2. +- Thanks to Gordon Williams and to everyone who contributes to Espruino and the Bangle.js 2 projects! +- SEGA, owners of the Puyo Puyo franchise that Bangle Blobs is based on. Please check out official Puyo Puyo games! +- Compile, the original creators of Puyo Puyo. The company went bankrupt long ago, but the people who worked for them continue to make games. diff --git a/apps/bblobface/app-icon.js b/apps/bblobface/app-icon.js new file mode 100644 index 000000000..e8d9baced --- /dev/null +++ b/apps/bblobface/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+HGm56+5BQ4JBAJItXAAoMMCJQAPJ5pfhJApPQL65HHKIbTU2nXAAu0I5xQNBo4tC2gAFGIxHIL5oNGEoItGGIgwDL6oMGFxgwFL6oVFFxwwEL7YuPGARfVBYwvUL6YLGL84THL84KHL7YHCL6AeBFx+0JggAGLx4wQFwa3DAIwvHNJQwMFwhgIEQ7ILGAYxHBAQWJADUeFAIAEjwtnjwAFGMglBFowxEGA/XgrgICJouMGA4aBAIgvMB4ouOGAouGMZgNGFx4wCPQ5hMN44vTK44wLNo5fUcRwuHL67iOHAxfhFxYJBBooeBFx8ecRY4KBowwOFxDgHM5BtHGBguZfhIkBGI4ICFyILFAIxBHAAoOGXIgLHBowBGFo0FAAoxHFxhfPAoQAJCIguNGxRtGABYpDQB72LFxwwEcCJfJFx4wCL7gvTADYv/F/4APYoQuOaoYwpFz4wOF0IwDGI4ICF0IxFAAgtFA=")) diff --git a/apps/bblobface/app.js b/apps/bblobface/app.js new file mode 100644 index 000000000..579a6bbb4 --- /dev/null +++ b/apps/bblobface/app.js @@ -0,0 +1,768 @@ +{ + // ~~ Variables for clock ~~ + let clockDrawTimeout; + let twelveHourTime = require('Storage').readJSON('setting.json', 1)['12hour']; + let updateSeconds = !Bangle.isLocked(); + let batteryLevel = E.getBattery(); + + // ~~ Variables for game logic ~~ + const NUM_COLORS = 6; + const NUISANCE_COLOR = 7; + let grid = [ + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]) + ]; + let hiddenRow = new Uint8Array([0, 0, 0, 0, 0, 0]); + let nextQueue = [{pivot: 1, leaf: 1}, {pivot: 1, leaf: 1}]; + let currentPair = {pivot: 0, leaf: 0}; + let dropCoordinates = {pivotX: 2, pivotY: 11, leafX: 2, leafY: 10}; + let pairX = 2; + let pairOrientation = 0; //0 is up, 1 is right, 2 is down, 3 is left + let slotsToCheck = []; + let selectedColors; + let lastChain = 0; + let gameLost = false; + let gamePaused = false; + let midChain = false; + + /* + Sets up a new game. + Must be called once before the first round. + */ + let restartGame = function() { + grid = [ + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]) + ]; + hiddenRow = new Uint8Array([0, 0, 0, 0, 0, 0]); + currentPair = {pivot: 0, leaf: 0}; + pairX = 2; + pairOrientation = 0; //0 is up, 1 is right, 2 is down, 3 is left + slotsToCheck = []; + gameLost = false; + lastChain = 0; + + //Set up random colors + selectedColors = new Uint8Array([1, 2, 3, 4, 5, 6]); + for (let i = NUM_COLORS - 1; i > 0; i--) { + let swap = selectedColors[i]; + let swapIndex = Math.floor(Math.random() * (i + 1)); + selectedColors[i] = selectedColors[swapIndex]; + selectedColors[swapIndex] = swap; + } + + //Create the first two pairs (Always in the first three colors) + nextQueue[0].pivot = selectedColors[Math.floor(Math.random() * 3)]; + nextQueue[0].leaf = selectedColors[Math.floor(Math.random() * 3)]; + nextQueue[1].pivot = selectedColors[Math.floor(Math.random() * 3)]; + nextQueue[1].leaf = selectedColors[Math.floor(Math.random() * 3)]; + }; + + /* + Readies the next pair and generates a new one for the queue. + */ + let newPair = function() { + currentPair.pivot = nextQueue[0].pivot; + currentPair.leaf = nextQueue[0].leaf; + + nextQueue[0].pivot = nextQueue[1].pivot; + nextQueue[0].leaf = nextQueue[1].leaf; + + nextQueue[1].pivot = selectedColors[Math.floor(Math.random() * 4)]; + nextQueue[1].leaf = selectedColors[Math.floor(Math.random() * 4)]; + + pairX = 2; + pairOrientation = 0; + + calcDropCoordinates(); + }; + + /* + Calculates the coordinates at which the current pair will be placed when quick dropped. + */ + let calcDropCoordinates = function() { + dropCoordinates.pivotX = pairX; + + //Find Y coordinate of pivot + dropCoordinates.pivotY = -2; + for (let i = 11; i >= 0; i--) { + if (grid[i][pairX] == 0) { + dropCoordinates.pivotY = i; + break; + } + } + if (dropCoordinates.pivotY == -2 && hiddenRow[pairX] == 0) + dropCoordinates.pivotY = -1; + + //Find coordinates of leaf + if (pairOrientation == 1) { + dropCoordinates.leafX = pairX + 1; + + dropCoordinates.leafY = -2; + for (let i = 11; i >= 0; i--) { + if (grid[i][pairX + 1] == 0) { + dropCoordinates.leafY = i; + break; + } + } + if (dropCoordinates.leafY == -2 && hiddenRow[pairX + 1] == 0) + dropCoordinates.leafY = -1; + } else if (pairOrientation == 3) { + dropCoordinates.leafX = pairX - 1; + + dropCoordinates.leafY = -2; + for (let i = 11; i >= 0; i--) { + if (grid[i][pairX - 1] == 0) { + dropCoordinates.leafY = i; + break; + } + } + if (dropCoordinates.leafY == -2 && hiddenRow[pairX - 1] == 0) + dropCoordinates.leafY = -1; + } else if (pairOrientation == 2) { + dropCoordinates.leafX = pairX; + dropCoordinates.leafY = dropCoordinates.pivotY; + dropCoordinates.pivotY--; + } else { + dropCoordinates.leafX = pairX; + dropCoordinates.leafY = dropCoordinates.pivotY - 1; + } + }; + + /* + Moves the current pair a certain number of slots. + */ + let movePair = function(dx) { + pairX += dx; + + if (dx < 0) { + if (pairX < (pairOrientation == 3 ? 1 : 0)) + pairX = (pairOrientation == 3 ? 1 : 0); + } + if (dx > 0) { + if (pairX > (pairOrientation == 1 ? 4 : 5)) + pairX = (pairOrientation == 1 ? 4 : 5); + } + + calcDropCoordinates(); + }; + + /* + Rotates the pair in the given direction around the pivot. + */ + let rotatePair = function(clockwise) { + pairOrientation += (clockwise ? 1 : -1); + if (pairOrientation > 3) + pairOrientation = 0; + if (pairOrientation < 0) + pairOrientation = 3; + + if (pairOrientation == 1 && pairX == 5) + pairX = 4; + if (pairOrientation == 3 && pairX == 0) + pairX = 1; + + calcDropCoordinates(); + }; + + /* + Places the current pair at the drop coordinates. + */ + let quickDrop = function() { + if (dropCoordinates.pivotY == -1) { + hiddenRow[dropCoordinates.pivotX] = currentPair.pivot; + } else if (dropCoordinates.pivotY > -1) { + grid[dropCoordinates.pivotY][dropCoordinates.pivotX] = currentPair.pivot; + } + + if (dropCoordinates.leafY == -1) { + hiddenRow[dropCoordinates.leafX] = currentPair.leaf; + } else if (dropCoordinates.leafY > -1) { + grid[dropCoordinates.leafY][dropCoordinates.leafX] = currentPair.leaf; + } + + currentPair.pivot = 0; + currentPair.leaf = 0; + }; + + /* + Makes all blobs fall to the lowest available slot. + All blobs that fall will be added to slotsToCheck. + */ + let settleBlobs = function() { + for (let x = 0; x < 6; x++) { + let lowestOpen = 11; + for (let y = 11; y >= 0; y--) { + if (grid[y][x] != 0) { + if (y != lowestOpen) { + grid[lowestOpen][x] = grid[y][x]; + grid[y][x] = 0; + addSlotToCheck(x, lowestOpen); + } + lowestOpen--; + } + } + + if (lowestOpen >= 0 && hiddenRow[x] != 0) { + grid[lowestOpen][x] = hiddenRow[x]; + hiddenRow[x] = 0; + addSlotToCheck(x, lowestOpen); + } + } + }; + + /* + Adds a slot to slotsToCheck. This slot will be checked for a pop + next time popAll is called. + */ + let addSlotToCheck = function(x, y) { + slotsToCheck.push({x: x, y: y}); + }; + + /* + Checks for a pop at every slot in slotsToCheck. + Pops at all locations. + */ + let popAll = function() { + let result = {pops: 0}; + while(slotsToCheck.length > 0) { + let coord = slotsToCheck.pop(); + if (grid[coord.y][coord.x] != 0 && grid[coord.y][coord.x] != NUISANCE_COLOR) { + if (checkSlotForPop(coord.x, coord.y)) + result.pops += 1; + } + } + return result; + }; + + /* + Checks a specific slot for a pop. + If there are four or more adjacent blobs of the same color, they are removed. + */ + let checkSlotForPop = function(x, y) { + let toDelete = [ + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]) + ]; + let blobsInClump = 0; + let color = grid[y][x]; + let toCheck = [{x: x, y: y}]; + + //Count every blob in this clump + while (toCheck.length > 0) { + let coord = toCheck.pop(); + if (grid[coord.y][coord.x] == color && toDelete[coord.y][coord.x] == 0) { + blobsInClump++; + toDelete[coord.y][coord.x] = 1; + if (coord.x > 0) toCheck.push({x: coord.x - 1, y: coord.y}); + if (coord.x < 5) toCheck.push({x: coord.x + 1, y: coord.y}); + if (coord.y > 0) toCheck.push({x: coord.x, y: coord.y - 1}); + if (coord.y < 11) toCheck.push({x: coord.x, y: coord.y + 1}); + } + if (grid[coord.y][coord.x] == NUISANCE_COLOR && toDelete[coord.y][coord.x] == 0) + toDelete[coord.y][coord.x] = 1; //For erasing garbage + } + + //If there are at least four blobs in this clump, remove them from the grid and draw a pop. + if (blobsInClump >= 4) { + for (let y = 0; y < 12; y++) { + for (let x = 0; x < 6; x++) { + if (toDelete[y][x] == 1) { + grid[y][x] = 0; + + //Clear the blob out of the slot + g.setBgColor(0, 0, 0); + g.clearRect((x*18)+34, (y*14)+7, (x*18)+52, (y*14)+21); + + //Draw the pop + let colorInfo = getColor(color); + g.setColor(colorInfo.r, colorInfo.g, colorInfo.b); + if (color < NUISANCE_COLOR) { + //A fancy pop for popped colors! + g.drawEllipse((x*18)+36, (y*14)+7, (x*18)+50, (y*14)+21); + g.drawEllipse((x*18)+27, (y*14)-2, (x*18)+59, (y*14)+30); + } else if (color == NUISANCE_COLOR) { + //Nuisance Blobs are simply crossed out. + //TODO: Nuisance Blobs are currently unusued, but also untested. Test before use. + g.drawLine((x*18)+34, (y*14)+7, (x*18)+52, (y*14)+21); + } + } + } + } + return true; + } + return false; + }; + + // Variables for graphics + let oldGhost = {pivotX: 0, pivotY: 0, leafX: 0, leafY: 0}; + + /* + Draws the time on the side. + */ + let drawTime = function(scheduleNext) { + //Change this to alter the y-coordinate of the top edge. + let dy = 25; + + g.setBgColor(0, 0, 0); + g.clearRect(2, dy, 30, dy + 121); + + //Draw the time + let d = new Date(); + let h = d.getHours(), m = d.getMinutes(); + if (twelveHourTime) { + let mer = 'A'; + if (h >= 12) mer = 'P'; + if (h >= 13) h -= 12; + if (h == 0) h = 12; + + g.setColor(1, 1, 1); + g.setFont("Vector", 12); + g.drawString(mer, 23, dy + 63); + } + let hs = h.toString().padStart(2, 0); + let ms = m.toString().padStart(2, 0); + g.setFont("Vector", 24); + g.setColor(1, 0.2, 1); + g.drawString(hs, 3, dy + 21); + g.setColor(0.5, 0.5, 1); + g.drawString(ms, 3, dy + 42); + + //Draw seconds + let s = d.getSeconds(); + if (updateSeconds) { + let ss = s.toString().padStart(2, 0); + g.setFont("Vector", 12); + g.setColor(0.2, 1, 0.2); + g.drawString(ss, 3, dy + 63); + } + + //Draw the date + let dayString = d.getDate().toString(); + let dayNames = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"]; + let dayName = dayNames[d.getDay()]; + let monthNames = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JLY", "AUG", "SEP", "OCT", "NOV", "DEC"]; + let monthName = monthNames[d.getMonth()]; + g.setColor(1, 1, 1); + g.setFont("Vector", 12); + g.drawString(monthName, 3, dy + 84); + g.drawString(dayString, 3, dy + 97); + g.setColor(0.5, 0.5, 0.5); + g.drawString(dayName, 3, dy + 110); + + //Draw battery + if (s == 0) batteryLevel = E.getBattery(); + if (Bangle.isCharging()) { + g.setColor(0, 0, 1); + } else if (batteryLevel <= 15) { + g.setColor(1, 0, 0); + } else { + g.setColor(0, 1, 0); + } + g.drawString(batteryLevel + "%", 3, dy + 1); + + //Schedule the next draw if requested. + if (!scheduleNext) return; + if (clockDrawTimeout) clearTimeout(clockDrawTimeout); + let interval = updateSeconds ? 1000 : 60000; + clockDrawTimeout = setTimeout(function() { + clockDrawTimeout = undefined; + drawTime(true); + }, interval - (Date.now() % interval)); + }; + + /* + Returns a tuple in the format {r, g, b} with the color + of the blob with the given ID. + This saves memory compared to having the colors stored in an array. + */ + let getColor = function(color) { + if (color == 1) + return {r: 1, g: 0, b: 0}; + if (color == 2) + return {r: 0, g: 1, b: 0}; + if (color == 3) + return {r: 0, g: 0, b: 1}; + if (color == 4) + return {r: 1, g: 1, b: 0}; + if (color == 5) + return {r: 1, g: 0, b: 1}; + if (color == 6) + return {r: 0, g: 1, b: 1}; + if (color == 7) + return {r: 0.5, g: 0.5, b: 0.5}; + return {r: 1, g: 1, b: 1}; + }; + + /* + Clears the screen and draws the background. + */ + let drawBackground = function() { + //Background + g.setBgColor(0.5, 0.2, 0.1); + g.clear(); + g.setBgColor(0, 0, 0); + g.clearRect(33, 0, 142, 176); + g.setBgColor(0.5, 0.5, 0.5); + g.clearRect(33, 4, 142, 6); + + //Reset button + g.setBgColor(0.5, 0.5, 0.5); + g.setColor(0, 0, 0); + g.clearRect(143, 150, 175, 175); + g.setFont("Vector", 30); + g.drawString("R", 152, 150); + + //Pause button + g.clearRect(0, 150, 32, 175); + g.fillRect(9, 154, 13, 171); + g.fillRect(18, 154, 22, 171); + }; + + /* + Draws a box under the next queue that displays + the current value of lastChain. + */ + let drawChainCount = function() { + g.setBgColor(0, 0, 0); + g.setColor(1, 0.2, 0.2); + g.setFont("Vector", 23); + g.clearRect(145, 42, 173, 64); + + if (lastChain > 0) { + if (lastChain < 10) g.drawString(lastChain, 154, 44); + if (lastChain >= 10) g.drawString(lastChain, 147, 44); + } + }; + + /* + Draws the blob at the given slot. + */ + let drawBlobAtSlot = function(x, y) { + //If this blob is in the hidden row, clear it out and stop. + if (y < 0) { + g.setBgColor(0, 0, 0); + g.clearRect((x*18)+34, 0, (x*18)+52, 3); + return; + } + + //First, clear what was in that slot. + g.setBgColor(0, 0, 0); + g.clearRect((x*18)+34, (y*14)+7, (x*18)+52, (y*14)+21); + + let color = grid[y][x]; + + if (color != 0) { + let myColor = getColor(color); + g.setColor(myColor.r, myColor.g, myColor.b); + g.fillEllipse((x*18)+34, (y*14)+7, (x*18)+52, (y*14)+21); + g.setColor(1, 1, 1); + g.drawEllipse((x*18)+34, (y*14)+7, (x*18)+52, (y*14)+21); + } + }; + + /* + Draws the ghost piece. + clearOld: if the previous location of the ghost piece should be cleared. + */ + let drawGhostPiece = function(clearOld) { + if (clearOld) { + g.setColor(0, 0, 0); + g.fillRect((oldGhost.pivotX*18)+38, (oldGhost.pivotY*14)+8, (oldGhost.pivotX*18)+47, (oldGhost.pivotY*14)+17); + g.fillRect((oldGhost.leafX*18)+38, (oldGhost.leafY*14)+8, (oldGhost.leafX*18)+47, (oldGhost.leafY*14)+17); + } + + let pivotX = dropCoordinates.pivotX; + let pivotY = dropCoordinates.pivotY; + let leafX = dropCoordinates.leafX; + let leafY = dropCoordinates.leafY; + let pivotColor = getColor(currentPair.pivot); + let leafColor = getColor(currentPair.leaf); + + g.setColor(pivotColor.r, pivotColor.g, pivotColor.b); + g.fillRect((pivotX*18)+40, (pivotY*14)+10, (pivotX*18)+45, (pivotY*14)+15); + g.setColor(1, 1, 1); + g.drawRect((pivotX*18)+38, (pivotY*14)+8, (pivotX*18)+47, (pivotY*14)+17); + g.setColor(leafColor.r, leafColor.g, leafColor.b); + g.fillRect((leafX*18)+40, (leafY*14)+10, (leafX*18)+45, (leafY*14)+15); + + oldGhost = {pivotX: pivotX, pivotY: pivotY, leafX: leafX, leafY: leafY}; + }; + + /* + Draws the next queue. + */ + let drawNextQueue = function() { + g.setBgColor(0, 0, 0); + g.clearRect(145, 4, 173, 28); + + let p1 = nextQueue[0].pivot; + let l1 = nextQueue[0].leaf; + let p2 = nextQueue[1].pivot; + let l2 = nextQueue[1].leaf; + let p1C = getColor(p1); + let l1C = getColor(l1); + let p2C = getColor(p2); + let l2C = getColor(l2); + + g.setColor(p1C.r, p1C.g, p1C.b); + g.fillEllipse(146, 17, 157, 28); + g.setColor(l1C.r, l1C.g, l1C.b); + g.fillEllipse(146, 5, 157, 16); + g.setColor(p2C.r, p2C.g, p2C.b); + g.fillEllipse(162, 17, 173, 28); + g.setColor(l2C.r, l2C.g, l2C.b); + g.fillEllipse(162, 5, 173, 16); + + g.setColor(1, 1, 1); + g.drawLine(159, 4, 159, 28); + g.drawEllipse(146, 17, 157, 28); + g.drawEllipse(146, 5, 157, 16); + g.drawEllipse(162, 17, 173, 28); + g.drawEllipse(162, 5, 173, 16); + }; + + /* + Redraws the screen, except for the ghost piece. + */ + let redrawBoard = function() { + drawBackground(); + drawNextQueue(); + drawChainCount(); + drawTime(false); + for (let y = 0; y < 12; y++) { + for (let x = 0; x < 6; x++) { + drawBlobAtSlot(x, y); + } + } + }; + + /* + Toggles the pause screen. + */ + let togglePause = function() { + gamePaused = !gamePaused; + + if (gamePaused) { + g.setBgColor(0.5, 0.2, 0.1); + g.clear(); + drawTime(false); + + g.setBgColor(0, 0, 0); + g.setColor(1, 1, 1); + g.clearRect(48, 66, 157, 110); + g.setFont("Vector", 20); + g.drawString("Tap here\nto unpause", 50, 68); + + require("widget_utils").show(); + Bangle.drawWidgets(); + } else { + require("widget_utils").hide(); + + redrawBoard(); + drawGhostPiece(false); + + //Display the loss text if the game is lost. + if (gameLost) { + g.setBgColor(0, 0, 0); + g.setColor(1, 1, 1); + g.clearRect(33, 73, 142, 103); + g.setFont("Vector", 20); + g.drawString("You Lose", 43, 80); + } + } + }; + + // ~~ Events ~~ + let dragAmnt = 0; + + let onTouch = (z, e) => { + if (midChain) return; + + if (gamePaused) { + if (e.x >= 40 && e.y >= 58 && e.x <= 165 && e.y <= 118) { + g.setBgColor(1, 1, 1); + g.clearRect(48, 66, 157, 110); + g.flip(); + togglePause(); + } + } else { + //Tap reset button + if (e.x >= 143 && e.y >= 150) { + restartGame(); + newPair(); + redrawBoard(); + drawGhostPiece(false); + g.flip(); + return; + } + + //Tap pause button + if (e.x <= 32 && e.y >= 150) { + togglePause(); + return; + } + + //While playing, rotate pieces. + if (!gameLost && !gamePaused) { + if (e.x < 88) { + rotatePair(false); + drawGhostPiece(true); + } else { + rotatePair(true); + drawGhostPiece(true); + } + } + } + }; + + Bangle.on("touch", onTouch); + + let onDrag = (e) => { + if (gameLost || gamePaused || midChain) return; + + //Do nothing if the user is dragging down so that they don't accidentally move while dropping + if (e.dy >= 5) { + return; + } + + dragAmnt += e.dx; + if (e.b == 0) { + dragAmnt = 0; + } + if (dragAmnt >= 20) { + movePair(Math.floor(dragAmnt / 20)); + drawGhostPiece(true); + dragAmnt = dragAmnt % 20; + } + if (dragAmnt <= -20) { + movePair(Math.ceil(dragAmnt / 20)); + drawGhostPiece(true); + dragAmnt = dragAmnt % 20; + } + }; + + Bangle.on("drag", onDrag); + + let onSwipe = (x, y) => { + if (gameLost || gamePaused || midChain) return; + + if (y > 0) { + let pivotX = dropCoordinates.pivotX; + let pivotY = dropCoordinates.pivotY; + let leafX = dropCoordinates.leafX; + let leafY = dropCoordinates.leafY; + + if (pivotY < -1 && leafY < -1) return; + + quickDrop(); + drawBlobAtSlot(pivotX, pivotY); + drawBlobAtSlot(leafX, leafY); + g.flip(); + + //Check for pops + if (pivotY >= 0) addSlotToCheck(pivotX, pivotY); + if (leafY >= 0) addSlotToCheck(leafX, leafY); + midChain = true; + let currentChain = 0; + while (popAll().pops > 0) { + currentChain++; + lastChain = currentChain; + drawChainCount(); + g.flip(); + settleBlobs(); + redrawBoard(); + g.flip(); + } + + newPair(); + drawNextQueue(); + drawGhostPiece(false); + + //If the top slot of the third column is taken, lose the game. + if (grid[0][2] != 0) { + gameLost = true; + g.setBgColor(0, 0, 0); + g.setColor(1, 1, 1); + g.clearRect(33, 73, 142, 103); + g.setFont("Vector", 20); + g.drawString("You Lose", 43, 80); + } + + midChain = false; + } + }; + + Bangle.on("swipe", onSwipe); + + let onLock = on => { + updateSeconds = !on; + drawTime(true); + }; + + Bangle.on('lock', onLock); + + let onCharging = charging => { + drawTime(false); + }; + + Bangle.on('charging', onCharging); + + Bangle.setUI({mode:"clock", remove:function() { + //Remove listeners + Bangle.removeListener("touch", onTouch); + Bangle.removeListener("drag", onDrag); + Bangle.removeListener("swipe", onSwipe); + Bangle.removeListener('lock', onLock); + Bangle.removeListener('charging', onCharging); + + if (clockDrawTimeout) clearTimeout(clockDrawTimeout); + require("widget_utils").show(); + }}); + + g.reset(); + + Bangle.loadWidgets(); + require("widget_utils").hide(); + + drawBackground(); + drawTime(true); + + restartGame(); + + newPair(); + drawGhostPiece(false); + + drawNextQueue(); + drawChainCount(); +} diff --git a/apps/bblobface/app.png b/apps/bblobface/app.png new file mode 100644 index 000000000..2201fa621 Binary files /dev/null and b/apps/bblobface/app.png differ diff --git a/apps/bblobface/metadata.json b/apps/bblobface/metadata.json new file mode 100644 index 000000000..6af247c91 --- /dev/null +++ b/apps/bblobface/metadata.json @@ -0,0 +1,15 @@ +{ "id": "bblobface", + "name": "Bangle Blobs Clock", + "shortName":"BBClock", + "icon": "app.png", + "version": "1.00", + "description": "A fully featured watch face with a playable game on the side.", + "readme":"README.md", + "type": "clock", + "tags": "clock, game", + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"bblobface.app.js","url":"app.js"}, + {"name":"bblobface.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/bblobface/screenshot1.png b/apps/bblobface/screenshot1.png new file mode 100644 index 000000000..91650c07a Binary files /dev/null and b/apps/bblobface/screenshot1.png differ diff --git a/apps/bblobface/screenshot2.png b/apps/bblobface/screenshot2.png new file mode 100644 index 000000000..64644965f Binary files /dev/null and b/apps/bblobface/screenshot2.png differ diff --git a/apps/calendar/interface.html b/apps/calendar/interface.html index 280a96c0b..ea64632f8 100644 --- a/apps/calendar/interface.html +++ b/apps/calendar/interface.html @@ -28,11 +28,16 @@ function readFile(input) { for(let i=0; i { - const jCalData = ICAL.parse(reader.result); + const icalText = reader.result.substring(reader.result.indexOf("BEGIN:VCALENDAR")); // remove html before data + const jCalData = ICAL.parse(icalText); const comp = new ICAL.Component(jCalData); + const vtz = comp.getFirstSubcomponent('vtimezone'); + const tz = new ICAL.Timezone(vtz); + // Fetch the VEVENT part comp.getAllSubcomponents('vevent').forEach(vevent => { - event = new ICAL.Event(vevent); + const event = new ICAL.Event(vevent); + event.startDate.zone = tz; holidays = holidays.filter(holiday => !sameDay(new Date(holiday.date), event.startDate.toJSDate())); // remove if already exists const holiday = eventToHoliday(event); diff --git a/apps/edgeclk/ChangeLog b/apps/edgeclk/ChangeLog index da75dfbae..b96d7207d 100644 --- a/apps/edgeclk/ChangeLog +++ b/apps/edgeclk/ChangeLog @@ -1,2 +1,3 @@ 0.01: Initial release. 0.02: Fix reset of progress bars on midnight. Fix display of 100k+ steps. +0.03: Added option to display weather. diff --git a/apps/edgeclk/README.md b/apps/edgeclk/README.md index 535a5e9df..90f6443fc 100644 --- a/apps/edgeclk/README.md +++ b/apps/edgeclk/README.md @@ -3,6 +3,7 @@ ![Screenshot](screenshot.png) ![Screenshot](screenshot2.png) ![Screenshot](screenshot3.png) +![Screenshot](screenshot4.png) Tinxx presents you a clock with as many straight edges as possible to allow for a crisp look and perfect readability. It comes with a custom font to display weekday, date, time, and steps. Also displays battery percentage while charging. @@ -15,6 +16,7 @@ The appearance is highly configurable. In the settings menu you can: - Switch between 24h and 12h clock. - Hide or display seconds.* - Show AM/PM in place of the seconds. +- Show weather temperature and icon in place of the seconds. - Set the daily step goal. - En- or disable the individual progress bars. - Set if your week should start with Monday or Sunday (for week progress bar). @@ -22,3 +24,8 @@ The appearance is highly configurable. In the settings menu you can: *) Hiding seconds should further reduce power consumption as the draw interval is prolonged as well. The clock implements Fast Loading for faster switching to and fro. + +## Contributors + - [tinxx](https://github.com/tinxx) + - [peerdavid](https://github.com/peerdavid) + \ No newline at end of file diff --git a/apps/edgeclk/app.js b/apps/edgeclk/app.js index 9f28e2588..f9d5f803b 100644 --- a/apps/edgeclk/app.js +++ b/apps/edgeclk/app.js @@ -8,6 +8,7 @@ twentyFourH: true, showAmPm: false, showSeconds: true, + showWeather: false, stepGoal: 10000, stepBar: true, weekBar: true, @@ -15,7 +16,6 @@ dayBar: true, }, require('Storage').readJSON('edgeclk.settings.json', true) || {}); - /* Runtime Variables ------------------------------------------------------------------------------*/ @@ -51,6 +51,33 @@ } else { drawSteps(stepsOnlyCount); } + + drawWeather(); + }; + + const drawWeather = function () { + if (!settings.showWeather){ + return; + } + + g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9) + g.setFontAlign(1, 1); // right bottom + + try{ + const weather = require('weather'); + const w = weather.get(); + let temp = parseInt(w.temp-273.15); + temp = temp < 0 ? '\\' + String(temp*-1) : String(temp); + + g.drawString(temp, g.getWidth()-40, g.getHeight() - 1, true); + + // clear icon area in case weather condition changed + g.clearRect(g.getWidth()-40, g.getHeight()-30, g.getWidth(), g.getHeight()); + weather.drawIcon(w, g.getWidth()-20, g.getHeight()-15, 14); + + } catch(e) { + g.drawString("???", g.getWidth()-3, g.getHeight() - 1, true); + } }; const drawDate = function (date) { @@ -135,7 +162,8 @@ g.setFontAlign(-1, 1); // left bottom const steps = Bangle.getHealthStatus('day').steps; - g.drawString((steps < 100000 ? steps.toString() : ((steps / 1000).toFixed(0) + 'K')).padEnd(5, '_'), + const toKSteps = settings.showWeather ? 1000 : 100000; + g.drawString((steps < toKSteps ? steps.toString() : ((steps / 1000).toFixed(0) + 'K')).padEnd(5, '_'), iconSize[0] + 6, g.getHeight() - 1, true); if (onlyCount === true) { @@ -229,12 +257,14 @@ // However, to save power while on battery only step count will get updated. // This will update icon and progress bar as well: if (!charging) drawSteps(); + drawWeather(); }; const onHealth = function () { if (!lcdPower || charging) return; // This will update progress bar and icon: drawSteps(); + drawWeather(); }; const onLock = function (locked) { diff --git a/apps/edgeclk/metadata.json b/apps/edgeclk/metadata.json index 3f72be77a..0d53cd008 100644 --- a/apps/edgeclk/metadata.json +++ b/apps/edgeclk/metadata.json @@ -2,11 +2,11 @@ "id": "edgeclk", "name": "Edge Clock", "shortName": "Edge Clock", - "version": "0.02", + "version": "0.03", "description": "Crisp clock with perfect readability.", "readme": "README.md", "icon": "app.png", - "screenshots": [{"url":"screenshot.png"}, {"url":"screenshot2.png"}, {"url":"screenshot3.png"}], + "screenshots": [{"url":"screenshot.png"}, {"url":"screenshot2.png"}, {"url":"screenshot3.png"}, {"url":"screenshot4.png"}], "type": "clock", "tags": "clock", "supports": ["BANGLEJS2"], diff --git a/apps/edgeclk/screenshot4.png b/apps/edgeclk/screenshot4.png new file mode 100644 index 000000000..66ec85c89 Binary files /dev/null and b/apps/edgeclk/screenshot4.png differ diff --git a/apps/edgeclk/settings.js b/apps/edgeclk/settings.js index 205dc5170..6f38e774c 100644 --- a/apps/edgeclk/settings.js +++ b/apps/edgeclk/settings.js @@ -8,6 +8,7 @@ twentyFourH: true, showAmPm: false, showSeconds: true, + showWeather: false, stepGoal: 10000, stepBar: true, weekBar: true, @@ -57,6 +58,7 @@ settings.showAmPm = !settings.showAmPm; // TODO can this be visually changed? if (settings.showAmPm && settings.showSeconds) settings.showSeconds = false; + if (settings.showAmPm && settings.showWeather) settings.showWeather = false; save(); }, }, @@ -66,6 +68,17 @@ settings.showSeconds = !settings.showSeconds; // TODO can this be visually changed? if (settings.showSeconds && settings.showAmPm) settings.showAmPm = false; + if (settings.showSeconds && settings.showWeather) settings.showWeather = false; + save(); + }, + }, + 'Show Weather': { + value: settings.showWeather, + onchange: () => { + settings.showWeather = !settings.showWeather; + // TODO can this be visually changed? + if (settings.showWeather && settings.showAmPm) settings.showAmPm = false; + if (settings.showWeather && settings.showSeconds) settings.showSeconds = false; save(); }, }, diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index 870ad0fdb..09637df1b 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -98,3 +98,12 @@ * New setting : power screen off between points to save battery * Color change for lost direction (now purple) * Adaptive screen powersaving + +0.21: + * Jit is back for display functions (10% speed increase) + * Store, parse and display elevation data + * Removed 'lost' indicator (we now change position to purple when lost) + * Powersaving fix : don't powersave when lost + * Bugfix for negative remaining distance when going backwards + * New settings for powersaving + * Adjustments to powersaving algorithm diff --git a/apps/gipy/README.md b/apps/gipy/README.md index 03ca97753..8539167e1 100644 --- a/apps/gipy/README.md +++ b/apps/gipy/README.md @@ -28,6 +28,7 @@ It provides the following features : - toilets - artwork - bakeries +- display elevation data if available in the trace ## Usage @@ -41,7 +42,7 @@ also a nice open source option. Note that *mapstogpx* has a super nice feature in its advanced settings. You can turn on 'next turn info' and be warned by the watch when you need to turn. -Once you have your gpx file you need to convert it to *gpc* which is my custom file format. +Once you have your gpx file you need to convert it to *gps* which is my custom file format. They are smaller than gpx and reduce the number of computations left to be done on the watch. Just click the disk icon and select your gpx file. @@ -78,34 +79,75 @@ On your screen you can see: * green: artwork - a *turn* indicator on the top right when you reach a turning point - a *gps* indicator (blinking) on the top right if you lose gps signal -- a *lost* indicator on the top right if you stray too far away from path ### Lost -If you stray away from path we will rescale the display to continue displaying nearby segments and -display the direction to follow as a purple segment. +If you stray away from path we will display the direction to follow as a purple segment. Your main position will also turn to purple. Note that while lost, the app will slow down a lot since it will start scanning all possible points to figure out where you are. On path it just needed to scan a few points ahead and behind. -The distance to next point displayed corresponds to the length of the black segment. +The distance to next point displayed corresponds to the length of the purple segment. ### Menu If you click the button you'll reach a menu where you can currently zoom out to see more of the map (with a slower refresh rate), reverse the path direction and disable power saving (keeping backlight on). +### Elevation + +If you touch the screen you will switch between display modes. +The first one displays the map, the second one the nearby elevation and the last one the elevation +for the whole path. + +![Screenshot](heights.png) + +Colors correspond to slopes. +Above 15% will be red, above 8% orange, above 3% yellow, between 3% and -3% is green and shades of blue +are for descents. + +You should note that the precision is not very good. The input data is not very precise and you only get the +slopes between path points. Don't expect to see small bumps on the road. + ### Settings Few settings for now (feel free to suggest me more) : -- lost distance : at which distance from path are you considered to be lost ? - buzz on turns : should the watch buzz when reaching a waypoint ? - disable bluetooth : turn bluetooth off completely to try to save some power. +- lost distance : at which distance from path are you considered to be lost ? +- wake-up speed : if you drive below this speed powersaving will disable itself +- active-time : how long (in seconds) the screen should be turned on if activated before going back to sleep. - brightness : how bright should screen be ? (by default 0.5, again saving power) - power lcd off (disabled by default): turn lcd off when inactive to save power. the watch will wake up when reaching points, when you touch the screen and when speed is below 13km/h. +### Powersaving + +Starting with release 0.20 we experiment with power saving. + +There are now two display modes : + +- active : the screen is lit back (default at 50% light but can be configured with the *brightness* setting) +- inactive : by default the screen is not lit but you can also power it off completely (with the *power lcd off* setting) + +The algorithm works in the following ways : + +- some events will *activate* : the display will turn *active* +- if no activation event occur for at least 10 seconds (or *active-time* setting) we switch back to *inactive* + +Activation events are the following : + +- you are near (< 100m) the next point on path +- you are slow (< *wake-up speed* setting (13 km/h by default)) +- you press the button / touch the screen + + +Power saving has been tested on a very long trip with several benefits + +- longer battery life +- waking up near path points will attract your attention more easily when needed + ### Caveats It is good to use but you should know : diff --git a/apps/gipy/TODO b/apps/gipy/TODO index b2a3c7ae1..8c767c463 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -36,10 +36,16 @@ my conclusion is that: ************************** +JIT: array declaration in jit is buggy +(especially several declarations) + +************************** + ++ try disabling gps for more powersaving + + when you walk the direction still has a tendency to shift + put back foot only ways -+ try fiddling with jit + put back street names + put back shortest paths but with points cache this time and jit + how to display paths from shortest path ? diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 071ef8283..46e29c359 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -7,24 +7,33 @@ let powersaving = true; let status; let interests_colors = [ - 0xffff, // Waypoints, white - 0xf800, // Bakery, red - 0x001f, // DrinkingWater, blue - 0x07ff, // Toilets, cyan - 0x07e0, // Artwork, green + 0xffff, // Waypoints, white + 0xf800, // Bakery, red + 0x001f, // DrinkingWater, blue + 0x07ff, // Toilets, cyan + 0x07e0, // Artwork, green ]; let Y_OFFSET = 20; + +// some constants for screen types +let MAP = 0; +let HEIGHTS_ZOOMED_IN = 1; +let HEIGHTS_FULL = 2; + let s = require("Storage"); -var settings = Object.assign({ - lost_distance: 50, - brightness: 0.5, - buzz_on_turns: false, - disable_bluetooth: true, - power_lcd_off: false, - }, - s.readJSON("gipy.json", true) || {} +var settings = Object.assign( + { + lost_distance: 50, + wake_up_speed: 13, + active_time: 10, + brightness: 0.5, + buzz_on_turns: false, + disable_bluetooth: true, + power_lcd_off: false, + }, + s.readJSON("gipy.json", true) || {} ); // let profile_start_times = []; @@ -41,25 +50,25 @@ var settings = Object.assign({ // return the index of the largest element of the array which is <= x function binary_search(array, x) { - let start = 0, - end = array.length; + let start = 0, + end = array.length; - while (end - start >= 0) { - let mid = Math.floor((start + end) / 2); - if (array[mid] == x) { - return mid; - } else if (array[mid] < x) { - if (array[mid + 1] > x) { - return mid; - } - start = mid + 1; - } else end = mid - 1; - } - if (array[start] > x) { - return null; - } else { - return start; - } + while (end - start >= 0) { + let mid = Math.floor((start + end) / 2); + if (array[mid] == x) { + return mid; + } else if (array[mid] < x) { + if (array[mid + 1] > x) { + return mid; + } + start = mid + 1; + } else end = mid - 1; + } + if (array[start] > x) { + return null; + } else { + return start; + } } // return a string containing estimated time of arrival. @@ -67,1513 +76,1735 @@ function binary_search(array, x) { // remaining distance in km // hour, minutes is current time function compute_eta(hour, minutes, approximate_speed, remaining_distance) { - if (isNaN(approximate_speed) || approximate_speed < 0.1) { - return ""; - } - let time_needed = (remaining_distance * 60) / approximate_speed; // in minutes - let eta_in_minutes = Math.round(hour * 60 + minutes + time_needed); - let eta_minutes = eta_in_minutes % 60; - let eta_hour = ((eta_in_minutes - eta_minutes) / 60) % 24; - if (eta_minutes < 10) { - return eta_hour.toString() + ":0" + eta_minutes; - } else { - return eta_hour.toString() + ":" + eta_minutes; - } + if (isNaN(approximate_speed) || approximate_speed < 0.1) { + return ""; + } + let time_needed = (remaining_distance * 60) / approximate_speed; // in minutes + let eta_in_minutes = Math.round(hour * 60 + minutes + time_needed); + let eta_minutes = eta_in_minutes % 60; + let eta_hour = ((eta_in_minutes - eta_minutes) / 60) % 24; + if (eta_minutes < 10) { + return eta_hour.toString() + ":0" + eta_minutes; + } else { + return eta_hour.toString() + ":" + eta_minutes; + } } class TilesOffsets { - constructor(buffer, offset) { - let type_size = Uint8Array(buffer, offset, 1)[0]; - offset += 1; - this.entry_size = Uint8Array(buffer, offset, 1)[0]; - offset += 1; - let non_empty_tiles_number = Uint16Array(buffer, offset, 1)[0]; - offset += 2; - this.non_empty_tiles = Uint16Array(buffer, offset, non_empty_tiles_number); - offset += 2 * non_empty_tiles_number; - if (type_size == 24) { - this.non_empty_tiles_ends = Uint24Array( - buffer, - offset, - non_empty_tiles_number - ); - offset += 3 * non_empty_tiles_number; - } else if (type_size == 16) { - this.non_empty_tiles_ends = Uint16Array( - buffer, - offset, - non_empty_tiles_number - ); - offset += 2 * non_empty_tiles_number; - } else { - throw "unknown size"; - } - return [this, offset]; + constructor(buffer, offset) { + let type_size = Uint8Array(buffer, offset, 1)[0]; + offset += 1; + this.entry_size = Uint8Array(buffer, offset, 1)[0]; + offset += 1; + let non_empty_tiles_number = Uint16Array(buffer, offset, 1)[0]; + offset += 2; + this.non_empty_tiles = Uint16Array(buffer, offset, non_empty_tiles_number); + offset += 2 * non_empty_tiles_number; + if (type_size == 24) { + this.non_empty_tiles_ends = Uint24Array( + buffer, + offset, + non_empty_tiles_number + ); + offset += 3 * non_empty_tiles_number; + } else if (type_size == 16) { + this.non_empty_tiles_ends = Uint16Array( + buffer, + offset, + non_empty_tiles_number + ); + offset += 2 * non_empty_tiles_number; + } else { + throw "unknown size"; } - tile_start_offset(tile_index) { - if (tile_index <= this.non_empty_tiles[0]) { - return 0; - } else { - return this.tile_end_offset(tile_index - 1); - } + return [this, offset]; + } + tile_start_offset(tile_index) { + if (tile_index <= this.non_empty_tiles[0]) { + return 0; + } else { + return this.tile_end_offset(tile_index - 1); } - tile_end_offset(tile_index) { - let me_or_before = binary_search(this.non_empty_tiles, tile_index); - if (me_or_before === null) { - return 0; - } - if (me_or_before >= this.non_empty_tiles_ends.length) { - return ( - this.non_empty_tiles_ends[this.non_empty_tiles.length - 1] * - this.entry_size - ); - } else { - return this.non_empty_tiles_ends[me_or_before] * this.entry_size; - } + } + tile_end_offset(tile_index) { + let me_or_before = binary_search(this.non_empty_tiles, tile_index); + if (me_or_before === null) { + return 0; } - end_offset() { - return ( - this.non_empty_tiles_ends[this.non_empty_tiles_ends.length - 1] * - this.entry_size - ); + if (me_or_before >= this.non_empty_tiles_ends.length) { + return ( + this.non_empty_tiles_ends[this.non_empty_tiles.length - 1] * + this.entry_size + ); + } else { + return this.non_empty_tiles_ends[me_or_before] * this.entry_size; } + } + end_offset() { + return ( + this.non_empty_tiles_ends[this.non_empty_tiles_ends.length - 1] * + this.entry_size + ); + } +} + +// this function is not inlined to avoid array declaration in jit +function center_points(points, scaled_current_x, scaled_current_y) { + return g.transformVertices(points, [ + 1, + 0, + 0, + 1, + -scaled_current_x, + -scaled_current_y, + ]); +} + +// this function is not inlined to avoid array declaration in jit +function rotate_points(points, c, s) { + let center_x = g.getWidth() / 2; + let center_y = g.getHeight() / 2 + Y_OFFSET; + + return g.transformVertices(points, [-c, s, s, c, center_x, center_y]); } class Map { - constructor(buffer, offset, filename) { - this.points_cache = []; // don't refetch points all the time - // header - let color_array = Uint8Array(buffer, offset, 3); - this.color = [ - color_array[0] / 255, - color_array[1] / 255, - color_array[2] / 255, - ]; - offset += 3; - this.first_tile = Int32Array(buffer, offset, 2); // absolute tile id of first tile - offset += 2 * 4; - this.grid_size = Uint32Array(buffer, offset, 2); // tiles width and height - offset += 2 * 4; - this.start_coordinates = Float64Array(buffer, offset, 2); // min x and y coordinates - offset += 2 * 8; - let side_array = Float64Array(buffer, offset, 1); // side of a tile - this.side = side_array[0]; - offset += 8; + constructor(buffer, offset, filename) { + this.points_cache = []; // don't refetch points all the time + // header + let color_array = Uint8Array(buffer, offset, 3); + this.color = [ + color_array[0] / 255, + color_array[1] / 255, + color_array[2] / 255, + ]; + offset += 3; + this.first_tile = Int32Array(buffer, offset, 2); // absolute tile id of first tile + offset += 2 * 4; + this.grid_size = Uint32Array(buffer, offset, 2); // tiles width and height + offset += 2 * 4; + this.start_coordinates = Float64Array(buffer, offset, 2); // min x and y coordinates + offset += 2 * 8; + let side_array = Float64Array(buffer, offset, 1); // side of a tile + this.side = side_array[0]; + offset += 8; - // tiles offsets - let res = new TilesOffsets(buffer, offset); - this.tiles_offsets = res[0]; - offset = res[1]; + // tiles offsets + let res = new TilesOffsets(buffer, offset); + this.tiles_offsets = res[0]; + offset = res[1]; - // now, do binary ways - // since the file is so big we'll go line by line - let binary_lines = []; - for (let y = 0; y < this.grid_size[1]; y++) { - let first_tile_start = this.tiles_offsets.tile_start_offset( - y * this.grid_size[0] + // now, do binary ways + // since the file is so big we'll go line by line + let binary_lines = []; + for (let y = 0; y < this.grid_size[1]; y++) { + let first_tile_start = this.tiles_offsets.tile_start_offset( + y * this.grid_size[0] + ); + let last_tile_end = this.tiles_offsets.tile_start_offset( + (y + 1) * this.grid_size[0] + ); + let size = last_tile_end - first_tile_start; + let string = s.read(filename, offset + first_tile_start, size); + let array = Uint8Array(E.toArrayBuffer(string)); + binary_lines.push(array); + } + this.binary_lines = binary_lines; + offset += this.tiles_offsets.end_offset(); + + return [this, offset]; + + // now do streets data header + // let streets_header = E.toArrayBuffer(s.read(filename, offset, 8)); + // let streets_header_offset = 0; + // let full_streets_size = Uint32Array( + // streets_header, + // streets_header_offset, + // 1 + // )[0]; + // streets_header_offset += 4; + // let blocks_number = Uint16Array( + // streets_header, + // streets_header_offset, + // 1 + // )[0]; + // streets_header_offset += 2; + // let labels_string_size = Uint16Array( + // streets_header, + // streets_header_offset, + // 1 + // )[0]; + // streets_header_offset += 2; + // offset += streets_header_offset; + + // // continue with main streets labels + // main_streets_labels = s.read(filename, offset, labels_string_size); + // // this.main_streets_labels = main_streets_labels.split(/\r?\n/); + // this.main_streets_labels = main_streets_labels.split(/\n/); + // offset += labels_string_size; + + // // continue with blocks start offsets + // this.blocks_offsets = Uint32Array( + // E.toArrayBuffer(s.read(filename, offset, blocks_number * 4)) + // ); + // offset += blocks_number * 4; + + // // continue with compressed street blocks + // let encoded_blocks_size = + // full_streets_size - 4 - 2 - 2 - labels_string_size - blocks_number * 4; + // this.compressed_streets = Uint8Array( + // E.toArrayBuffer(s.read(filename, offset, encoded_blocks_size)) + // ); + // offset += encoded_blocks_size; + } + + display( + displayed_x, + displayed_y, + scale_factor, + cos_direction, + sin_direction + ) { + g.setColor(this.color[0], this.color[1], this.color[2]); + let local_x = displayed_x - this.start_coordinates[0]; + let local_y = displayed_y - this.start_coordinates[1]; + let tile_x = Math.floor(local_x / this.side); + let tile_y = Math.floor(local_y / this.side); + + let limit = 1; + if (!zoomed) { + limit = 2; + } + for (let y = tile_y - limit; y <= tile_y + limit; y++) { + if (y < 0 || y >= this.grid_size[1]) { + continue; + } + for (let x = tile_x - limit; x <= tile_x + limit; x++) { + if (x < 0 || x >= this.grid_size[0]) { + continue; + } + if ( + this.tile_is_on_screen( + x, + y, + local_x, + local_y, + scale_factor, + cos_direction, + sin_direction + ) + ) { + // let colors = [ + // [0, 0, 0], + // [0, 0, 1], + // [0, 1, 0], + // [0, 1, 1], + // [1, 0, 0], + // [1, 0, 1], + // [1, 1, 0], + // [1, 1, 0.5], + // [0.5, 0, 0.5], + // [0, 0.5, 0.5], + // ]; + if (this.color[0] == 1 && this.color[1] == 0 && this.color[2] == 0) { + this.display_thick_tile( + x, + y, + local_x, + local_y, + scale_factor, + cos_direction, + sin_direction ); - let last_tile_end = this.tiles_offsets.tile_start_offset( - (y + 1) * this.grid_size[0] + } else { + this.display_tile( + x, + y, + local_x, + local_y, + scale_factor, + cos_direction, + sin_direction ); - let size = last_tile_end - first_tile_start; - let string = s.read(filename, offset + first_tile_start, size); - let array = Uint8Array(E.toArrayBuffer(string)); - binary_lines.push(array); + } } - this.binary_lines = binary_lines; - offset += this.tiles_offsets.end_offset(); - - return [this, offset]; - - // now do streets data header - // let streets_header = E.toArrayBuffer(s.read(filename, offset, 8)); - // let streets_header_offset = 0; - // let full_streets_size = Uint32Array( - // streets_header, - // streets_header_offset, - // 1 - // )[0]; - // streets_header_offset += 4; - // let blocks_number = Uint16Array( - // streets_header, - // streets_header_offset, - // 1 - // )[0]; - // streets_header_offset += 2; - // let labels_string_size = Uint16Array( - // streets_header, - // streets_header_offset, - // 1 - // )[0]; - // streets_header_offset += 2; - // offset += streets_header_offset; - - // // continue with main streets labels - // main_streets_labels = s.read(filename, offset, labels_string_size); - // // this.main_streets_labels = main_streets_labels.split(/\r?\n/); - // this.main_streets_labels = main_streets_labels.split(/\n/); - // offset += labels_string_size; - - // // continue with blocks start offsets - // this.blocks_offsets = Uint32Array( - // E.toArrayBuffer(s.read(filename, offset, blocks_number * 4)) - // ); - // offset += blocks_number * 4; - - // // continue with compressed street blocks - // let encoded_blocks_size = - // full_streets_size - 4 - 2 - 2 - labels_string_size - blocks_number * 4; - // this.compressed_streets = Uint8Array( - // E.toArrayBuffer(s.read(filename, offset, encoded_blocks_size)) - // ); - // offset += encoded_blocks_size; + } } + } - display( - displayed_x, - displayed_y, - scale_factor, - cos_direction, - sin_direction - ) { - g.setColor(this.color[0], this.color[1], this.color[2]); - let local_x = displayed_x - this.start_coordinates[0]; - let local_y = displayed_y - this.start_coordinates[1]; - let tile_x = Math.floor(local_x / this.side); - let tile_y = Math.floor(local_y / this.side); + tile_is_on_screen( + tile_x, + tile_y, + current_x, + current_y, + scale_factor, + cos_direction, + sin_direction + ) { + let width = g.getWidth(); + let height = g.getHeight(); + let center_x = width / 2; + let center_y = height / 2 + Y_OFFSET; + let side = this.side; + let tile_center_x = (tile_x + 0.5) * side; + let tile_center_y = (tile_y + 0.5) * side; + let scaled_center_x = (tile_center_x - current_x) * scale_factor; + let scaled_center_y = (tile_center_y - current_y) * scale_factor; + let rotated_center_x = + scaled_center_x * cos_direction - scaled_center_y * sin_direction; + let rotated_center_y = + scaled_center_x * sin_direction + scaled_center_y * cos_direction; + let on_screen_center_x = center_x - rotated_center_x; + let on_screen_center_y = center_y + rotated_center_y; - let limit = 1; - if (!zoomed) { - limit = 2; - } - for (let y = tile_y - limit; y <= tile_y + limit; y++) { - if (y < 0 || y >= this.grid_size[1]) { - continue; - } - for (let x = tile_x - limit; x <= tile_x + limit; x++) { - if (x < 0 || x >= this.grid_size[0]) { - continue; - } - if ( - this.tile_is_on_screen( - x, - y, - local_x, - local_y, - scale_factor, - cos_direction, - sin_direction - ) - ) { - // let colors = [ - // [0, 0, 0], - // [0, 0, 1], - // [0, 1, 0], - // [0, 1, 1], - // [1, 0, 0], - // [1, 0, 1], - // [1, 1, 0], - // [1, 1, 0.5], - // [0.5, 0, 0.5], - // [0, 0.5, 0.5], - // ]; - if (this.color[0] == 1 && this.color[1] == 0 && this.color[2] == 0) { - this.display_thick_tile( - x, - y, - local_x, - local_y, - scale_factor, - cos_direction, - sin_direction - ); - } else { - this.display_tile( - x, - y, - local_x, - local_y, - scale_factor, - cos_direction, - sin_direction - ); - } - } - } - } + let scaled_side = side * scale_factor * Math.sqrt(1 / 2); + + if (on_screen_center_x + scaled_side <= 0) { + return false; } - - tile_is_on_screen( - tile_x, - tile_y, - current_x, - current_y, - scale_factor, - cos_direction, - sin_direction - ) { - let width = g.getWidth(); - let height = g.getHeight(); - let center_x = width / 2; - let center_y = height / 2 + Y_OFFSET; - let side = this.side; - let tile_center_x = (tile_x + 0.5) * side; - let tile_center_y = (tile_y + 0.5) * side; - let scaled_center_x = (tile_center_x - current_x) * scale_factor; - let scaled_center_y = (tile_center_y - current_y) * scale_factor; - let rotated_center_x = scaled_center_x * cos_direction - scaled_center_y * sin_direction; - let rotated_center_y = scaled_center_x * sin_direction + scaled_center_y * cos_direction; - let on_screen_center_x = center_x - rotated_center_x; - let on_screen_center_y = center_y + rotated_center_y; - - let scaled_side = side * scale_factor * Math.sqrt(1 / 2); - - if (on_screen_center_x + scaled_side <= 0) { - return false; - } - if (on_screen_center_x - scaled_side >= width) { - return false; - } - if (on_screen_center_y + scaled_side <= 0) { - return false; - } - if (on_screen_center_y - scaled_side >= height) { - return false; - } - return true; + if (on_screen_center_x - scaled_side >= width) { + return false; } - - tile_points(tile_num, tile_x, tile_y, scaled_side) { - let line_start_offset = this.tiles_offsets.tile_start_offset( - tile_y * this.grid_size[0] - ); - let offset = - this.tiles_offsets.tile_start_offset(tile_num) - line_start_offset; - let upper_limit = - this.tiles_offsets.tile_end_offset(tile_num) - line_start_offset; - - let line = this.binary_lines[tile_y]; - // we need to copy both for correct results and for performances - // let's precompute also. - let cached_tile = new Float64Array(upper_limit - offset); - for (let i = offset; i < upper_limit; i += 2) { - let x = (tile_x + line.buffer[i] / 255) * scaled_side; - let y = (tile_y + line.buffer[i + 1] / 255) * scaled_side; - cached_tile[i - offset] = x; - cached_tile[i + 1 - offset] = y; - } - return cached_tile; + if (on_screen_center_y + scaled_side <= 0) { + return false; } - - invalidate_caches() { - this.points_cache = []; + if (on_screen_center_y - scaled_side >= height) { + return false; } + return true; + } - fetch_points(tile_x, tile_y, scaled_side) { - let tile_num = tile_x + tile_y * this.grid_size[0]; - for (let i = 0; i < this.points_cache.length; i++) { - if (this.points_cache[i][0] == tile_num) { - return this.points_cache[i][1]; - } - } - if (this.points_cache.length > 40) { - this.points_cache.shift(); - } - let points = this.tile_points(tile_num, tile_x, tile_y, scaled_side); - this.points_cache.push([tile_num, points]); - return points; + tile_points(tile_num, tile_x, tile_y, scaled_side) { + let line_start_offset = this.tiles_offsets.tile_start_offset( + tile_y * this.grid_size[0] + ); + let offset = + this.tiles_offsets.tile_start_offset(tile_num) - line_start_offset; + let upper_limit = + this.tiles_offsets.tile_end_offset(tile_num) - line_start_offset; + + let line = this.binary_lines[tile_y]; + // we need to copy both for correct results and for performances + // let's precompute also. + let cached_tile = new Float64Array(upper_limit - offset); + for (let i = offset; i < upper_limit; i += 2) { + let x = (tile_x + line.buffer[i] / 255) * scaled_side; + let y = (tile_y + line.buffer[i + 1] / 255) * scaled_side; + cached_tile[i - offset] = x; + cached_tile[i + 1 - offset] = y; } + return cached_tile; + } - display_tile( - tile_x, - tile_y, - current_x, - current_y, - scale_factor, - cos_direction, - sin_direction - ) { - // "jit"; - let center_x = g.getWidth() / 2; - let center_y = g.getHeight() / 2 + Y_OFFSET; + invalidate_caches() { + this.points_cache = []; + } - let points = this.fetch_points(tile_x, tile_y, this.side * scale_factor); - let scaled_current_x = current_x * scale_factor; - let scaled_current_y = current_y * scale_factor; - let recentered_points = g.transformVertices(points, [1, 0, 0, 1, -scaled_current_x, -scaled_current_y]); - let c = cos_direction; - let s = sin_direction; - let screen_points = g.transformVertices(recentered_points, [-c, s, s, c, center_x, center_y]); - - for (let i = 0; i < screen_points.length; i += 4) { - g.drawLine(screen_points[i], screen_points[i + 1], screen_points[i + 2], screen_points[i + 3]); - } + fetch_points(tile_x, tile_y, scaled_side) { + let tile_num = tile_x + tile_y * this.grid_size[0]; + for (let i = 0; i < this.points_cache.length; i++) { + if (this.points_cache[i][0] == tile_num) { + return this.points_cache[i][1]; + } } - - display_thick_tile( - tile_x, - tile_y, - current_x, - current_y, - scale_factor, - cos_direction, - sin_direction - ) { - // "jit"; - let center_x = g.getWidth() / 2; - let center_y = g.getHeight() / 2 + Y_OFFSET; - - let points = this.fetch_points(tile_x, tile_y, this.side * scale_factor); - let scaled_current_x = current_x * scale_factor; - let scaled_current_y = current_y * scale_factor; - let recentered_points = g.transformVertices(points, [1, 0, 0, 1, -scaled_current_x, -scaled_current_y]); - let c = cos_direction; - let s = sin_direction; - let screen_points = g.transformVertices(recentered_points, [-c, s, s, c, center_x, center_y]); - - for (let i = 0; i < screen_points.length; i += 4) { - let final_x = screen_points[i]; - let final_y = screen_points[i + 1]; - let new_final_x = screen_points[i + 2]; - let new_final_y = screen_points[i + 3]; - - let xdiff = new_final_x - final_x; - let ydiff = new_final_y - final_y; - let d = Math.sqrt(xdiff * xdiff + ydiff * ydiff); - let ox = (-ydiff / d) * 3; - let oy = (xdiff / d) * 3; - g.fillPoly([ - final_x + ox, - final_y + oy, - new_final_x + ox, - new_final_y + oy, - new_final_x - ox, - new_final_y - oy, - final_x - ox, - final_y - oy, - ]); - } + if (this.points_cache.length > 40) { + this.points_cache.shift(); } + let points = this.tile_points(tile_num, tile_x, tile_y, scaled_side); + this.points_cache.push([tile_num, points]); + return points; + } + + display_tile( + tile_x, + tile_y, + current_x, + current_y, + scale_factor, + cos_direction, + sin_direction + ) { + "jit"; + let points = this.fetch_points(tile_x, tile_y, this.side * scale_factor); + let scaled_current_x = current_x * scale_factor; + let scaled_current_y = current_y * scale_factor; + let recentered_points = center_points( + points, + scaled_current_x, + scaled_current_y + ); + let screen_points = rotate_points( + recentered_points, + cos_direction, + sin_direction + ); + + for (let i = 0; i < screen_points.length; i += 4) { + g.drawLine( + screen_points[i], + screen_points[i + 1], + screen_points[i + 2], + screen_points[i + 3] + ); + } + } + + display_thick_tile( + tile_x, + tile_y, + current_x, + current_y, + scale_factor, + cos_direction, + sin_direction + ) { + "jit"; + + let points = this.fetch_points(tile_x, tile_y, this.side * scale_factor); + let scaled_current_x = current_x * scale_factor; + let scaled_current_y = current_y * scale_factor; + let recentered_points = center_points( + points, + scaled_current_x, + scaled_current_y + ); + let screen_points = rotate_points( + recentered_points, + cos_direction, + sin_direction + ); + + for (let i = 0; i < screen_points.length; i += 4) { + let final_x = screen_points[i]; + let final_y = screen_points[i + 1]; + let new_final_x = screen_points[i + 2]; + let new_final_y = screen_points[i + 3]; + + let xdiff = new_final_x - final_x; + let ydiff = new_final_y - final_y; + let d = Math.sqrt(xdiff * xdiff + ydiff * ydiff); + let ox = (-ydiff / d) * 3; + let oy = (xdiff / d) * 3; + g.fillPoly([ + final_x + ox, + final_y + oy, + new_final_x + ox, + new_final_y + oy, + new_final_x - ox, + new_final_y - oy, + final_x - ox, + final_y - oy, + ]); + } + } } class Interests { - constructor(buffer, offset) { - this.first_tile = Int32Array(buffer, offset, 2); // absolute tile id of first tile - offset += 2 * 4; - this.grid_size = Uint32Array(buffer, offset, 2); // tiles width and height - offset += 2 * 4; - this.start_coordinates = Float64Array(buffer, offset, 2); // min x and y coordinates - offset += 2 * 8; - let side_array = Float64Array(buffer, offset, 1); // side of a tile - this.side = side_array[0]; - offset += 8; + constructor(buffer, offset) { + this.first_tile = Int32Array(buffer, offset, 2); // absolute tile id of first tile + offset += 2 * 4; + this.grid_size = Uint32Array(buffer, offset, 2); // tiles width and height + offset += 2 * 4; + this.start_coordinates = Float64Array(buffer, offset, 2); // min x and y coordinates + offset += 2 * 8; + let side_array = Float64Array(buffer, offset, 1); // side of a tile + this.side = side_array[0]; + offset += 8; - let res = new TilesOffsets(buffer, offset); - offset = res[1]; - this.offsets = res[0]; - let end = this.offsets.end_offset(); - this.binary_interests = new Uint8Array(end); - let binary_interests = Uint8Array(buffer, offset, end); - for (let i = 0; i < end; i++) { - this.binary_interests[i] = binary_interests[i]; - } - offset += end; - this.points_cache = []; - return [this, offset]; + let res = new TilesOffsets(buffer, offset); + offset = res[1]; + this.offsets = res[0]; + let end = this.offsets.end_offset(); + this.binary_interests = new Uint8Array(end); + let binary_interests = Uint8Array(buffer, offset, end); + for (let i = 0; i < end; i++) { + this.binary_interests[i] = binary_interests[i]; } + offset += end; + this.points_cache = []; + return [this, offset]; + } - display( - displayed_x, - displayed_y, - scale_factor, - cos_direction, - sin_direction - ) { - let local_x = displayed_x - this.start_coordinates[0]; - let local_y = displayed_y - this.start_coordinates[1]; - let tile_x = Math.floor(local_x / this.side); - let tile_y = Math.floor(local_y / this.side); - for (let y = tile_y - 1; y <= tile_y + 1; y++) { - if (y < 0 || y >= this.grid_size[1]) { - continue; - } - for (let x = tile_x - 1; x <= tile_x + 1; x++) { - if (x < 0 || x >= this.grid_size[0]) { - continue; - } - this.display_tile( - x, - y, - local_x, - local_y, - scale_factor, - cos_direction, - sin_direction - ); - } + display( + displayed_x, + displayed_y, + scale_factor, + cos_direction, + sin_direction + ) { + let local_x = displayed_x - this.start_coordinates[0]; + let local_y = displayed_y - this.start_coordinates[1]; + let tile_x = Math.floor(local_x / this.side); + let tile_y = Math.floor(local_y / this.side); + for (let y = tile_y - 1; y <= tile_y + 1; y++) { + if (y < 0 || y >= this.grid_size[1]) { + continue; + } + for (let x = tile_x - 1; x <= tile_x + 1; x++) { + if (x < 0 || x >= this.grid_size[0]) { + continue; } + this.display_tile( + x, + y, + local_x, + local_y, + scale_factor, + cos_direction, + sin_direction + ); + } } + } - tile_points(tile_num, tile_x, tile_y, scaled_side) { - let offset = this.offsets.tile_start_offset(tile_num); - let upper_limit = this.offsets.tile_end_offset(tile_num); + tile_points(tile_num, tile_x, tile_y, scaled_side) { + let offset = this.offsets.tile_start_offset(tile_num); + let upper_limit = this.offsets.tile_end_offset(tile_num); - let tile_interests = []; - for (let i = offset; i < upper_limit; i += 3) { - let interest = this.binary_interests[i]; - let x = (tile_x + this.binary_interests[i + 1] / 255) * scaled_side; - let y = (tile_y + this.binary_interests[i + 2] / 255) * scaled_side; - if (interest >= interests_colors.length) { - throw "bad interest" + interest + "at" + tile_num + "offset" + i; - } - tile_interests.push(interest); - tile_interests.push(x); - tile_interests.push(y); - } - return tile_interests; + let tile_interests = []; + for (let i = offset; i < upper_limit; i += 3) { + let interest = this.binary_interests[i]; + let x = (tile_x + this.binary_interests[i + 1] / 255) * scaled_side; + let y = (tile_y + this.binary_interests[i + 2] / 255) * scaled_side; + if (interest >= interests_colors.length) { + throw "bad interest" + interest + "at" + tile_num + "offset" + i; + } + tile_interests.push(interest); + tile_interests.push(x); + tile_interests.push(y); } - fetch_points(tile_x, tile_y, scaled_side) { - //TODO: factorize with map ? - let tile_num = tile_x + tile_y * this.grid_size[0]; - for (let i = 0; i < this.points_cache.length; i++) { - if (this.points_cache[i][0] == tile_num) { - return this.points_cache[i][1]; - } - } - if (this.points_cache.length > 40) { - this.points_cache.shift(); - } - let points = this.tile_points(tile_num, tile_x, tile_y, scaled_side); - this.points_cache.push([tile_num, points]); - return points; + return tile_interests; + } + fetch_points(tile_x, tile_y, scaled_side) { + //TODO: factorize with map ? + let tile_num = tile_x + tile_y * this.grid_size[0]; + for (let i = 0; i < this.points_cache.length; i++) { + if (this.points_cache[i][0] == tile_num) { + return this.points_cache[i][1]; + } } - invalidate_caches() { - this.points_cache = []; + if (this.points_cache.length > 40) { + this.points_cache.shift(); } - display_tile( - tile_x, - tile_y, - displayed_x, - displayed_y, - scale_factor, - cos_direction, - sin_direction - ) { - let width = g.getWidth(); - let half_width = width / 2; - let half_height = g.getHeight() / 2 + Y_OFFSET; - let interests = this.fetch_points(tile_x, tile_y, this.side * scale_factor); + let points = this.tile_points(tile_num, tile_x, tile_y, scaled_side); + this.points_cache.push([tile_num, points]); + return points; + } + invalidate_caches() { + this.points_cache = []; + } + display_tile( + tile_x, + tile_y, + displayed_x, + displayed_y, + scale_factor, + cos_direction, + sin_direction + ) { + let width = g.getWidth(); + let half_width = width / 2; + let half_height = g.getHeight() / 2 + Y_OFFSET; + let interests = this.fetch_points(tile_x, tile_y, this.side * scale_factor); - let scaled_current_x = displayed_x * scale_factor; - let scaled_current_y = displayed_y * scale_factor; + let scaled_current_x = displayed_x * scale_factor; + let scaled_current_y = displayed_y * scale_factor; - for (let i = 0; i < interests.length; i += 3) { - let type = interests[i]; - let x = interests[i + 1]; - let y = interests[i + 2]; + for (let i = 0; i < interests.length; i += 3) { + let type = interests[i]; + let x = interests[i + 1]; + let y = interests[i + 2]; - let scaled_x = x - scaled_current_x; - let scaled_y = y - scaled_current_y; - let rotated_x = scaled_x * cos_direction - scaled_y * sin_direction; - let rotated_y = scaled_x * sin_direction + scaled_y * cos_direction; - let final_x = half_width - rotated_x; - let final_y = half_height + rotated_y; + let scaled_x = x - scaled_current_x; + let scaled_y = y - scaled_current_y; + let rotated_x = scaled_x * cos_direction - scaled_y * sin_direction; + let rotated_y = scaled_x * sin_direction + scaled_y * cos_direction; + let final_x = half_width - rotated_x; + let final_y = half_height + rotated_y; - let color = interests_colors[type]; - if (type == 0) { - g.setColor(0, 0, 0).fillCircle(final_x, final_y, 6); - } - g.setColor(color).fillCircle(final_x, final_y, 5); - } + let color = interests_colors[type]; + if (type == 0) { + g.setColor(0, 0, 0).fillCircle(final_x, final_y, 6); + } + g.setColor(color).fillCircle(final_x, final_y, 5); } + } } class Status { - constructor(path, maps, interests) { - this.path = path; - this.default_options = true; // do we still have default options ? - this.active = false; // should we have screen on - this.last_activity = getTime(); - this.maps = maps; - this.interests = interests; - let half_screen_width = g.getWidth() / 2; - let half_screen_height = g.getHeight() / 2; - let half_screen_diagonal = Math.sqrt( - half_screen_width * half_screen_width + - half_screen_height * half_screen_height - ); - this.scale_factor = half_screen_diagonal / maps[0].side; // multiply geo coordinates by this to get pixels coordinates - this.on_path = true; // are we on the path or lost ? - this.position = null; // where we are - this.adjusted_cos_direction = 1; // cos of where we look at - this.adjusted_sin_direction = 0; // sin of where we look at - this.current_segment = null; // which segment is closest - this.reaching = null; // which waypoint are we reaching ? - this.distance_to_next_point = null; // how far are we from next point ? - this.projected_point = null; + constructor(path, maps, interests, heights) { + this.path = path; + this.default_options = true; // do we still have default options ? + this.active = false; // should we have screen on + this.last_activity = getTime(); + this.maps = maps; + this.interests = interests; + this.heights = heights; + this.screen = MAP; + let half_screen_width = g.getWidth() / 2; + let half_screen_height = g.getHeight() / 2; + let half_screen_diagonal = Math.sqrt( + half_screen_width * half_screen_width + + half_screen_height * half_screen_height + ); + this.scale_factor = half_screen_diagonal / maps[0].side; // multiply geo coordinates by this to get pixels coordinates + this.on_path = true; // are we on the path or lost ? + this.position = null; // where we are + this.adjusted_cos_direction = 1; // cos of where we look at + this.adjusted_sin_direction = 0; // sin of where we look at + this.current_segment = null; // which segment is closest + this.reaching = null; // which waypoint are we reaching ? + this.distance_to_next_point = null; // how far are we from next point ? + this.projected_point = null; - if (this.path !== null) { - let r = [0]; - // let's do a reversed prefix computations on all distances: - // loop on all segments in reversed order - let previous_point = null; - for (let i = this.path.len - 1; i >= 0; i--) { - let point = this.path.point(i); - if (previous_point !== null) { - r.unshift(r[0] + point.distance(previous_point)); - } - previous_point = point; - } - this.remaining_distances = r; // how much distance remains at start of each segment + if (this.path !== null) { + let r = [0]; + // let's do a reversed prefix computations on all distances: + // loop on all segments in reversed order + let previous_point = null; + for (let i = this.path.len - 1; i >= 0; i--) { + let point = this.path.point(i); + if (previous_point !== null) { + r.unshift(r[0] + point.distance(previous_point)); } - this.starting_time = null; // time we start - this.advanced_distance = 0.0; - this.gps_coordinates_counter = 0; // how many coordinates did we receive - this.old_points = []; // record previous points but only when enough distance between them - this.old_times = []; // the corresponding times + previous_point = point; + } + this.remaining_distances = r; // how much distance remains at start of each segment } - activate() { - this.last_activity = getTime(); - if (this.active) { - return; - } else { - this.active = true; - Bangle.setLCDBrightness(settings.brightness); - Bangle.setLocked(false); - if (settings.power_lcd_off) { - Bangle.setLCDPower(true); - } - } + this.starting_time = null; // time we start + this.advanced_distance = 0.0; + this.gps_coordinates_counter = 0; // how many coordinates did we receive + this.old_points = []; // record previous points but only when enough distance between them + this.old_times = []; // the corresponding times + } + activate() { + this.last_activity = getTime(); + if (this.active) { + return; + } else { + this.active = true; + Bangle.setLCDBrightness(settings.brightness); + Bangle.setLocked(false); + if (settings.power_lcd_off) { + Bangle.setLCDPower(true); + } } - check_activity() { - if (!this.active || !powersaving) { - return; - } - if (getTime() - this.last_activity > 30) { - this.active = false; - Bangle.setLCDBrightness(0); - if (settings.power_lcd_off) { - Bangle.setLCDPower(false); - } - } + } + check_activity() { + if (!this.active || !powersaving) { + return; } - invalidate_caches() { - for (let i = 0; i < this.maps.length; i++) { - this.maps[i].invalidate_caches(); - } - if (this.interests !== null) { - this.interests.invalidate_caches(); - } + if (getTime() - this.last_activity > settings.active_time) { + this.active = false; + Bangle.setLCDBrightness(0); + if (settings.power_lcd_off) { + Bangle.setLCDPower(false); + } } - new_position_reached(position) { - // we try to figure out direction by looking at previous points - // instead of the gps course which is not very nice. + } + invalidate_caches() { + for (let i = 0; i < this.maps.length; i++) { + this.maps[i].invalidate_caches(); + } + if (this.interests !== null) { + this.interests.invalidate_caches(); + } + } + new_position_reached(position) { + // we try to figure out direction by looking at previous points + // instead of the gps course which is not very nice. - let now = getTime(); - - if (this.old_points.length == 0) { - this.gps_coordinates_counter += 1; - this.old_points.push(position); - this.old_times.push(now); - return null; - } else { - let previous_point = this.old_points[this.old_points.length - 1]; - let distance_to_previous = previous_point.distance(position); - // gps signal is noisy but rarely above 5 meters - if (distance_to_previous < 5) { - // update instant speed and return - let oldest_point = this.old_points[0]; - let distance_to_oldest = oldest_point.distance(position); - this.instant_speed = distance_to_oldest / (now - this.old_times[0]); - return null; - } - } - this.gps_coordinates_counter += 1; - this.old_points.push(position); - this.old_times.push(now); + let now = getTime(); + if (this.old_points.length == 0) { + this.gps_coordinates_counter += 1; + this.old_points.push(position); + this.old_times.push(now); + return null; + } else { + let previous_point = this.old_points[this.old_points.length - 1]; + let distance_to_previous = previous_point.distance(position); + // gps signal is noisy but rarely above 5 meters + if (distance_to_previous < 5) { + // update instant speed and return let oldest_point = this.old_points[0]; let distance_to_oldest = oldest_point.distance(position); - - // every 3 points we count the distance - if (this.gps_coordinates_counter % 3 == 0) { - if (distance_to_oldest < 150.0) { - // to avoid gps glitches - this.advanced_distance += distance_to_oldest; - } - } - this.instant_speed = distance_to_oldest / (now - this.old_times[0]); - - if (this.old_points.length == 4) { - this.old_points.shift(); - this.old_times.shift(); - } - // let's just take angle of segment between newest point and a point a bit before - let previous_index = this.old_points.length - 3; - if (previous_index < 0) { - previous_index = 0; - } - let diff = position.minus(this.old_points[previous_index]); - let angle = Math.atan2(diff.lat, diff.lon); - return angle; + return null; + } } - update_position(new_position) { - let direction = this.new_position_reached(new_position); - if (direction === null) { - if (this.old_points.length > 1) { - this.display(); // re-display because speed has changed - } - return; - } - if (in_menu) { - return; - } - if (this.instant_speed * 3.6 < 13) { - this.activate(); // if we go too slow turn on, we might be looking for the direction to follow - if (!this.default_options) { - this.default_options = true; + this.gps_coordinates_counter += 1; + this.old_points.push(position); + this.old_times.push(now); - Bangle.setOptions({ - lockTimeout: 10000, - backlightTimeout: 10000, - wakeOnTwist: true, - powerSave: true, - }); - } - } else { - if (this.default_options) { - this.default_options = false; + let oldest_point = this.old_points[0]; + let distance_to_oldest = oldest_point.distance(position); - Bangle.setOptions({ - lockTimeout: 0, - backlightTimeout: 0, - lcdPowerTimeout: 0, - hrmSportMode: 2, - wakeOnTwist: false, // if true watch will never sleep due to speed and road bumps. tried several tresholds. - wakeOnFaceUp: false, - wakeOnTouch: true, - powerSave: false, - }); - Bangle.setPollInterval(2000); // disable accelerometer as much as we can (a value of 4000 seem to cause hard reboot crashes (segfaults ?) so keep 2000) - } - - } - this.check_activity(); // if we don't move or are in menu we should stay on - - this.adjusted_cos_direction = Math.cos(-direction - Math.PI / 2.0); - this.adjusted_sin_direction = Math.sin(-direction - Math.PI / 2.0); - this.angle = direction; - let cos_direction = Math.cos(direction); - let sin_direction = Math.sin(direction); - this.position = new_position; - - // we will display position of where we'll be at in a few seconds - // and not where we currently are. - // this is because the display has more than 1sec duration. - this.displayed_position = new Point( - new_position.lon + cos_direction * this.instant_speed * 0.00001, - new_position.lat + sin_direction * this.instant_speed * 0.00001 - ); - - if (this.path !== null) { - // detect segment we are on now - let res = this.path.nearest_segment( - this.displayed_position, - Math.max(0, this.current_segment - 1), - Math.min(this.current_segment + 2, this.path.len - 1), - cos_direction, - sin_direction - ); - let orientation = res[0]; - let next_segment = res[1]; - - if (this.is_lost(next_segment)) { - // start_profiling(); - // it did not work, try anywhere - res = this.path.nearest_segment( - this.displayed_position, - 0, - this.path.len - 1, - cos_direction, - sin_direction - ); - orientation = res[0]; - next_segment = res[1]; - // end_profiling("repositioning"); - } - // now check if we strayed away from path or back to it - let lost = this.is_lost(next_segment); - if (this.on_path == lost) { - this.activate(); - // if status changes - if (lost) { - Bangle.buzz(); // we lost path - setTimeout(() => Bangle.buzz(), 500); - setTimeout(() => Bangle.buzz(), 1000); - setTimeout(() => Bangle.buzz(), 1500); - } - this.on_path = !lost; - } - - this.current_segment = next_segment; - - // check if we are nearing the next point on our path and alert the user - let next_point = this.current_segment + (1 - orientation); - this.distance_to_next_point = Math.ceil( - this.position.distance(this.path.point(next_point)) - ); - - // disable gps when far from next point and locked - // if (Bangle.isLocked() && !settings.keep_gps_alive) { - // let time_to_next_point = - // (this.distance_to_next_point * 3.6) / settings.max_speed; - // if (time_to_next_point > 60) { - // Bangle.setGPSPower(false, "gipy"); - // setTimeout(function () { - // Bangle.setGPSPower(true, "gipy"); - // }, time_to_next_point); - // } - // } - if (this.reaching != next_point && this.distance_to_next_point <= 100) { - this.activate(); - this.reaching = next_point; - let reaching_waypoint = this.path.is_waypoint(next_point); - if (reaching_waypoint) { - if (settings.buzz_on_turns) { - Bangle.buzz(); - setTimeout(() => Bangle.buzz(), 500); - setTimeout(() => Bangle.buzz(), 1000); - setTimeout(() => Bangle.buzz(), 1500); - } - } - } - } - - // abort most frames if inactive - if (!this.active && this.gps_coordinates_counter % 5 != 0) { - return; - } - - // re-display - this.display(); + // every 3 points we count the distance + if (this.gps_coordinates_counter % 3 == 0) { + if (distance_to_oldest < 150.0) { + // to avoid gps glitches + this.advanced_distance += distance_to_oldest; + } } - display_direction() { - //TODO: go towards point on path at 20 meter - if (this.current_segment === null) { - return; - } - let next_point = this.path.point(this.current_segment + (1 - go_backwards)); - let distance_to_next_point = Math.ceil( - this.projected_point.distance(next_point) - ); - let towards; - if (distance_to_next_point < 20) { - towards = this.path.point(this.current_segment + 2 * (1 - go_backwards)); - } else { - towards = next_point; - } - let diff = towards.minus(this.projected_point); - direction = Math.atan2(diff.lat, diff.lon); + this.instant_speed = distance_to_oldest / (now - this.old_times[0]); - let full_angle = direction - this.angle; - // let c = towards.coordinates(p, this.adjusted_cos_direction, this.adjusted_sin_direction, this.scale_factor); - // g.setColor(1,0,1).fillCircle(c[0], c[1], 5); - - let scale; - if (zoomed) { - scale = this.scale_factor; - } else { - scale = this.scale_factor / 2; - } - - c = this.projected_point.coordinates( - this.displayed_position, - this.adjusted_cos_direction, - this.adjusted_sin_direction, - scale - ); - - let cos1 = Math.cos(full_angle + 0.6 + Math.PI / 2); - let cos2 = Math.cos(full_angle + Math.PI / 2); - let cos3 = Math.cos(full_angle - 0.6 + Math.PI / 2); - let sin1 = Math.sin(-full_angle - 0.6 - Math.PI / 2); - let sin2 = Math.sin(-full_angle - Math.PI / 2); - let sin3 = Math.sin(-full_angle + 0.6 - Math.PI / 2); - g.setColor(0, 1, 0).fillPoly([ - c[0] + cos1 * 15, - c[1] + sin1 * 15, - c[0] + cos2 * 20, - c[1] + sin2 * 20, - c[0] + cos3 * 15, - c[1] + sin3 * 15, - c[0] + cos3 * 10, - c[1] + sin3 * 10, - c[0] + cos2 * 15, - c[1] + sin2 * 15, - c[0] + cos1 * 10, - c[1] + sin1 * 10, - ]); + if (this.old_points.length == 4) { + this.old_points.shift(); + this.old_times.shift(); } - remaining_distance() { - let remaining_in_correct_orientation = - this.remaining_distances[this.current_segment + 1] + - this.position.distance(this.path.point(this.current_segment + 1)); - - if (go_backwards) { - return this.remaining_distances[0] - remaining_in_correct_orientation; - } else { - return remaining_in_correct_orientation; - } + // let's just take angle of segment between newest point and a point a bit before + let previous_index = this.old_points.length - 3; + if (previous_index < 0) { + previous_index = 0; } - // check if we are lost (too far from segment we think we are on) - // if we are adjust scale so that path will still be displayed. - // we do the scale adjustment here to avoid recomputations later on. - is_lost(segment) { - let projection = this.displayed_position.closest_segment_point( - this.path.point(segment), - this.path.point(segment + 1) - ); - this.projected_point = projection; // save this info for display - let distance_to_projection = this.displayed_position.distance(projection); - if (distance_to_projection > settings.lost_distance) { - return true; - } else { - return false; - } + let diff = position.minus(this.old_points[previous_index]); + let angle = Math.atan2(diff.lat, diff.lon); + return angle; + } + update_position(new_position) { + let direction = this.new_position_reached(new_position); + if (direction === null) { + if (this.old_points.length > 1) { + this.display(); // re-display because speed has changed + } + return; } - display() { - if (displaying || in_menu) { - return; // don't draw on drawings - } - displaying = true; - g.clear(); - let scale_factor = this.scale_factor; - if (!zoomed) { - scale_factor /= 2; - } + if (in_menu) { + return; + } + if (this.instant_speed * 3.6 < settings.wake_up_speed) { + this.activate(); // if we go too slow turn on, we might be looking for the direction to follow + if (!this.default_options) { + this.default_options = true; + Bangle.setOptions({ + lockTimeout: 0, + backlightTimeout: 10000, + wakeOnTwist: true, + powerSave: true, + }); + } + } else { + if (this.default_options) { + this.default_options = false; + + Bangle.setOptions({ + lockTimeout: 0, + backlightTimeout: 0, + lcdPowerTimeout: 0, + hrmSportMode: 2, + wakeOnTwist: false, // if true watch will never sleep due to speed and road bumps. tried several tresholds. + wakeOnFaceUp: false, + wakeOnTouch: true, + powerSave: false, + }); + } + } + this.check_activity(); // if we don't move or are in menu we should stay on + + this.adjusted_cos_direction = Math.cos(-direction - Math.PI / 2.0); + this.adjusted_sin_direction = Math.sin(-direction - Math.PI / 2.0); + this.angle = direction; + let cos_direction = Math.cos(direction); + let sin_direction = Math.sin(direction); + this.position = new_position; + + // we will display position of where we'll be at in a few seconds + // and not where we currently are. + // this is because the display has more than 1sec duration. + this.displayed_position = new Point( + new_position.lon + cos_direction * this.instant_speed * 0.00001, + new_position.lat + sin_direction * this.instant_speed * 0.00001 + ); + + if (this.path !== null) { + // detect segment we are on now + let res = this.path.nearest_segment( + this.displayed_position, + Math.max(0, this.current_segment - 1), + Math.min(this.current_segment + 2, this.path.len - 1), + cos_direction, + sin_direction + ); + let orientation = res[0]; + let next_segment = res[1]; + + if (this.is_lost(next_segment)) { // start_profiling(); - for (let i = 0; i < this.maps.length; i++) { - this.maps[i].display( - this.displayed_position.lon, - this.displayed_position.lat, - scale_factor, - this.adjusted_cos_direction, - this.adjusted_sin_direction - ); - } - // end_profiling("map"); - if (this.interests !== null) { - this.interests.display( - this.displayed_position.lon, - this.displayed_position.lat, - scale_factor, - this.adjusted_cos_direction, - this.adjusted_sin_direction - ); - } - if (this.position !== null) { - this.display_path(); - } - - this.display_direction(); - this.display_stats(); - Bangle.drawWidgets(); - displaying = false; - } - display_stats() { - let now = new Date(); - let minutes = now.getMinutes().toString(); - if (minutes.length < 2) { - minutes = "0" + minutes; - } - let hours = now.getHours().toString(); - - // display the clock - g.setFont("6x8:2") - .setFontAlign(-1, -1, 0) - .setColor(g.theme.fg) - .drawString(hours + ":" + minutes, 0, 24); - - let approximate_speed; - // display speed (avg and instant) - if (this.old_times.length > 0) { - let point_time = this.old_times[this.old_times.length - 1]; - let done_in = point_time - this.starting_time; - approximate_speed = Math.round( - (this.advanced_distance * 3.6) / done_in - ); - let approximate_instant_speed = Math.round(this.instant_speed * 3.6); - g.setFont("6x8:2") - .setFontAlign(-1, -1, 0) - .drawString( - "" + - approximate_speed + - "km/h", - 0, - g.getHeight() - 15 - ); - - g.setFont("6x8:3") - .setFontAlign(1, -1, 0) - .drawString( - "" + approximate_instant_speed, - g.getWidth(), - g.getHeight() - 22 - ); - } - - if (this.path === null || this.position === null) { - return; - } - - let remaining_distance = this.remaining_distance(); - let rounded_distance = Math.round(remaining_distance / 100) / 10; - let total = Math.round(this.remaining_distances[0] / 100) / 10; - // now, distance to next point in meters - g.setFont("6x8:2") - .setFontAlign(-1, -1, 0) - .setColor(g.theme.fg) - .drawString( - "" + this.distance_to_next_point + "m", - 0, - g.getHeight() - 49 - ); - - let forward_eta = compute_eta( - now.getHours(), - now.getMinutes(), - approximate_speed, - remaining_distance / 1000 + // it did not work, try anywhere + res = this.path.nearest_segment( + this.displayed_position, + 0, + this.path.len - 1, + cos_direction, + sin_direction ); + orientation = res[0]; + next_segment = res[1]; + // end_profiling("repositioning"); + } + // now check if we strayed away from path or back to it + let lost = this.is_lost(next_segment); + if (this.on_path == lost) { + // if status changes + if (lost) { + Bangle.buzz(); // we lost path + setTimeout(() => Bangle.buzz(), 500); + setTimeout(() => Bangle.buzz(), 1000); + setTimeout(() => Bangle.buzz(), 1500); + } + this.on_path = !lost; + } + if (!this.on_path) { + this.activate(); + } - // now display ETA - g.setFont("6x8:2") - .setFontAlign(-1, -1, 0) - .setColor(g.theme.fg) - .drawString(forward_eta, 0, 42); + this.current_segment = next_segment; - // display distance on path - g.setFont("6x8:2").drawString( - "" + rounded_distance + "/" + total, - 0, - g.getHeight() - 32 + // check if we are nearing the next point on our path and alert the user + let next_point = this.current_segment + (1 - orientation); + this.distance_to_next_point = Math.ceil( + this.position.distance(this.path.point(next_point)) + ); + + // disable gps when far from next point and locked + // if (Bangle.isLocked() && !settings.keep_gps_alive) { + // let time_to_next_point = + // (this.distance_to_next_point * 3.6) / settings.max_speed; + // if (time_to_next_point > 60) { + // Bangle.setGPSPower(false, "gipy"); + // setTimeout(function () { + // Bangle.setGPSPower(true, "gipy"); + // }, time_to_next_point); + // } + // } + if (this.distance_to_next_point <= 100) { + this.activate(); + } + if (this.reaching != next_point && this.distance_to_next_point <= 100) { + this.reaching = next_point; + let reaching_waypoint = this.path.is_waypoint(next_point); + if (reaching_waypoint) { + if (settings.buzz_on_turns) { + Bangle.buzz(); + setTimeout(() => Bangle.buzz(), 500); + setTimeout(() => Bangle.buzz(), 1000); + setTimeout(() => Bangle.buzz(), 1500); + } + } + } + } + + // abort most frames if inactive + if (!this.active && this.gps_coordinates_counter % 5 != 0) { + return; + } + + // re-display + this.display(); + } + display_direction() { + //TODO: go towards point on path at 20 meter + if (this.current_segment === null) { + return; + } + let next_point = this.path.point(this.current_segment + (1 - go_backwards)); + + let distance_to_next_point = Math.ceil( + this.projected_point.distance(next_point) + ); + let towards; + if (distance_to_next_point < 20) { + towards = this.path.point(this.current_segment + 2 * (1 - go_backwards)); + } else { + towards = next_point; + } + let diff = towards.minus(this.projected_point); + direction = Math.atan2(diff.lat, diff.lon); + + let full_angle = direction - this.angle; + // let c = towards.coordinates(p, this.adjusted_cos_direction, this.adjusted_sin_direction, this.scale_factor); + // g.setColor(1,0,1).fillCircle(c[0], c[1], 5); + + let scale; + if (zoomed) { + scale = this.scale_factor; + } else { + scale = this.scale_factor / 2; + } + + c = this.projected_point.coordinates( + this.displayed_position, + this.adjusted_cos_direction, + this.adjusted_sin_direction, + scale + ); + + let cos1 = Math.cos(full_angle + 0.6 + Math.PI / 2); + let cos2 = Math.cos(full_angle + Math.PI / 2); + let cos3 = Math.cos(full_angle - 0.6 + Math.PI / 2); + let sin1 = Math.sin(-full_angle - 0.6 - Math.PI / 2); + let sin2 = Math.sin(-full_angle - Math.PI / 2); + let sin3 = Math.sin(-full_angle + 0.6 - Math.PI / 2); + g.setColor(0, 1, 0).fillPoly([ + c[0] + cos1 * 15, + c[1] + sin1 * 15, + c[0] + cos2 * 20, + c[1] + sin2 * 20, + c[0] + cos3 * 15, + c[1] + sin3 * 15, + c[0] + cos3 * 10, + c[1] + sin3 * 10, + c[0] + cos2 * 15, + c[1] + sin2 * 15, + c[0] + cos1 * 10, + c[1] + sin1 * 10, + ]); + } + remaining_distance() { + if (go_backwards) { + return this.remaining_distances[0] - this.remaining_distances[this.current_segment] + + this.position.distance(this.path.point(this.current_segment)); + } else { + return this.remaining_distances[this.current_segment + 1] + + this.position.distance(this.path.point(this.current_segment + 1)); + } + } + // check if we are lost (too far from segment we think we are on) + // if we are adjust scale so that path will still be displayed. + // we do the scale adjustment here to avoid recomputations later on. + is_lost(segment) { + let projection = this.displayed_position.closest_segment_point( + this.path.point(segment), + this.path.point(segment + 1) + ); + this.projected_point = projection; // save this info for display + let distance_to_projection = this.displayed_position.distance(projection); + if (distance_to_projection > settings.lost_distance) { + return true; + } else { + return false; + } + } + display() { + if (displaying || in_menu) { + return; // don't draw on drawings + } + displaying = true; + g.clear(); + if (this.screen == MAP) { + this.display_map(); + } else { + let current_position = 0; + if (this.current_segment !== null) { + if (go_backwards) { + current_position = this.remaining_distance(); + } else { + current_position = + this.remaining_distances[0] - this.remaining_distance(); + } + } + if (this.screen == HEIGHTS_FULL) { + this.display_heights(0, current_position, this.remaining_distances[0]); + } else { + // only display 2500m + let start; + if (go_backwards) { + start = Math.max(0, current_position - 2000); + } else { + start = Math.max(0, current_position - 500); + } + let length = Math.min(2500, this.remaining_distances[0] - start); + this.display_heights(start, current_position, length); + } + } + Bangle.drawWidgets(); + displaying = false; + } + display_heights(display_start, current_position, displayed_length) { + let path_length = this.remaining_distances[0]; + let widgets_height = 24; + let graph_width = g.getWidth(); + let graph_height = g.getHeight() - 20 - widgets_height; + + let distance_per_pixel = displayed_length / graph_width; + + let start_point_index = 0; + let end_point_index = this.path.len - 1; + for (let i = 0; i < this.path.len; i++) { + let point_distance = path_length - this.remaining_distances[i]; + if (point_distance <= display_start) { + start_point_index = i; + } + if (point_distance >= display_start + displayed_length) { + end_point_index = i; + break; + } + } + end_point_index = Math.min(end_point_index+1, this.path.len -1); + let max_height = Number.NEGATIVE_INFINITY; + let min_height = Number.POSITIVE_INFINITY; + for (let i = start_point_index; i <= end_point_index; i++) { + let height = this.heights[i]; + max_height = Math.max(max_height, height); + min_height = Math.min(min_height, height); + } + // we'll set the displayed height to a minimum value of 100m + // if we don't, then we'll see too much noise + if (max_height - min_height < 100) { + min_height = min_height - 10; + max_height = min_height + 110; + } + + let displayed_height = max_height - min_height; + let height_per_pixel = displayed_height / graph_height; + // g.setColor(0, 0, 0).drawRect(0, widgets_height, graph_width, graph_height + widgets_height); + + let previous_x = null; + let previous_y = null; + let previous_height = null; + let previous_distance = null; + let current_x; + let current_y; + for (let i = start_point_index; i < end_point_index; i++) { + let point_distance = path_length - this.remaining_distances[i]; + let height = this.heights[i]; + let x = Math.round((point_distance - display_start) / distance_per_pixel); + if (go_backwards) { + x = graph_width - x; + } + let y = + widgets_height + + graph_height - + Math.round((height - min_height) / height_per_pixel); + if (x != previous_x) { + if (previous_x !== null) { + let steepness = + (height - previous_height) / (point_distance - previous_distance); + if (go_backwards) { + steepness *= -1; + } + let color; + if (steepness > 0.15) { + color = "#ff0000"; + } else if (steepness > 0.8) { + color = "#ff8000"; + } else if (steepness > 0.03) { + color = "#ffff00"; + } else if (steepness > -0.03) { + color = "#00ff00"; + } else if (steepness > -0.08) { + color = "#00aa44"; + } else if (steepness > -0.015) { + color = "#0044aa"; + } else { + color = "#0000ff"; + } + g.setColor(color); + g.fillPoly([ + previous_x, + previous_y, + x, + y, + x, + widgets_height + graph_height, + previous_x, + widgets_height + graph_height, + ]); + if ( + current_position >= previous_distance && + current_position < point_distance + ) { + let current_height = + previous_height + + ((current_position - previous_distance) / + (point_distance - previous_distance)) * + (height - previous_height); + current_x = Math.round( + (current_position - display_start) / distance_per_pixel + ); + if (go_backwards) { + current_x = graph_width - current_x; + } + current_y = + widgets_height + + graph_height - + Math.round((current_height - min_height) / height_per_pixel); + } + } + previous_distance = point_distance; + previous_height = height; + previous_x = x; + previous_y = y; + } + } + if (this.on_path) { + g.setColor(0, 0, 0); + } else { + g.setColor(1, 0, 1); + } + g.fillCircle(current_x, current_y, 5); + + // display min dist/max dist and min height/max height + g.setColor(g.theme.fg); + g.setFont("6x8:2"); + g.setFontAlign(-1, 1, 0).drawString( + Math.ceil(display_start / 100) / 10, + 0, + g.getHeight() + ); + + g.setFontAlign(1, 1, 0).drawString( + Math.ceil((display_start + displayed_length) / 100) / 10, + g.getWidth(), + g.getHeight() + ); + + g.setFontAlign(1, 1, 0).drawString( + min_height, + g.getWidth(), + widgets_height + graph_height + ); + g.setFontAlign(1, -1, 0).drawString( + max_height, + g.getWidth(), + widgets_height + ); + } + display_map() { + let scale_factor = this.scale_factor; + if (!zoomed) { + scale_factor /= 2; + } + + // start_profiling(); + for (let i = 0; i < this.maps.length; i++) { + this.maps[i].display( + this.displayed_position.lon, + this.displayed_position.lat, + scale_factor, + this.adjusted_cos_direction, + this.adjusted_sin_direction + ); + } + // end_profiling("map"); + if (this.interests !== null) { + this.interests.display( + this.displayed_position.lon, + this.displayed_position.lat, + scale_factor, + this.adjusted_cos_direction, + this.adjusted_sin_direction + ); + } + if (this.position !== null) { + this.display_path(); + } + + this.display_direction(); + this.display_stats(); + } + display_stats() { + let now = new Date(); + let minutes = now.getMinutes().toString(); + if (minutes.length < 2) { + minutes = "0" + minutes; + } + let hours = now.getHours().toString(); + + // display the clock + g.setFont("6x8:2") + .setFontAlign(-1, -1, 0) + .setColor(g.theme.fg) + .drawString(hours + ":" + minutes, 0, 24); + + let approximate_speed; + // display speed (avg and instant) + if (this.old_times.length > 0) { + let point_time = this.old_times[this.old_times.length - 1]; + let done_in = point_time - this.starting_time; + approximate_speed = Math.round((this.advanced_distance * 3.6) / done_in); + let approximate_instant_speed = Math.round(this.instant_speed * 3.6); + g.setFont("6x8:2") + .setFontAlign(-1, -1, 0) + .drawString("" + approximate_speed + "km/h", 0, g.getHeight() - 15); + + g.setFont("6x8:3") + .setFontAlign(1, -1, 0) + .drawString( + "" + approximate_instant_speed, + g.getWidth(), + g.getHeight() - 22 ); - - // display various indicators - if (this.distance_to_next_point <= 100) { - if (this.path.is_waypoint(this.reaching)) { - g.setColor(0.0, 1.0, 0.0) - .setFont("6x15") - .drawString("turn", g.getWidth() - 50, 30); - } - } - if (!this.on_path) { - g.setColor(1.0, 0.0, 0.0) - .setFont("6x15") - .drawString("lost", g.getWidth() - 55, 35); - } } - display_path() { - // don't display all segments, only those neighbouring current segment - // this is most likely to be the correct display - // while lowering the cost a lot - // - // note that all code is inlined here to speed things up - let cos = this.adjusted_cos_direction; - let sin = this.adjusted_sin_direction; - let displayed_x = this.displayed_position.lon; - let displayed_y = this.displayed_position.lat; - let width = g.getWidth(); - let height = g.getHeight(); - let half_width = width / 2; - let half_height = height / 2 + Y_OFFSET; - let scale_factor = this.scale_factor; - if (!zoomed) { - scale_factor /= 2; - } - if (this.path !== null) { - // compute coordinate for projection on path - let tx = (this.projected_point.lon - displayed_x) * scale_factor; - let ty = (this.projected_point.lat - displayed_y) * scale_factor; - let rotated_x = tx * cos - ty * sin; - let rotated_y = tx * sin + ty * cos; - let projected_x = half_width - Math.round(rotated_x); // x is inverted - let projected_y = half_height + Math.round(rotated_y); - - // display direction to next point if lost - if (!this.on_path) { - let next_point = this.path.point(this.current_segment + 1); - let previous_point = this.path.point(this.current_segment); - let nearest_point; - if ( - previous_point.fake_distance(this.position) < - next_point.fake_distance(this.position) - ) { - nearest_point = previous_point; - } else { - nearest_point = next_point; - } - let tx = (nearest_point.lon - displayed_x) * scale_factor; - let ty = (nearest_point.lat - displayed_y) * scale_factor; - let rotated_x = tx * cos - ty * sin; - let rotated_y = tx * sin + ty * cos; - let x = half_width - Math.round(rotated_x); // x is inverted - let y = half_height + Math.round(rotated_y); - g.setColor(1, 0, 1).drawLine(half_width, half_height, x, y); - } - - // display current-segment's projection - g.setColor(0, 0, 0); - g.fillCircle(projected_x, projected_y, 4); - } - - // now display ourselves - g.setColor(0, 0, 0); - g.fillCircle(half_width, half_height, 5); + if (this.path === null || this.position === null) { + return; } + + let remaining_distance = this.remaining_distance(); + let rounded_distance = Math.round(remaining_distance / 100) / 10; + let total = Math.round(this.remaining_distances[0] / 100) / 10; + // now, distance to next point in meters + g.setFont("6x8:2") + .setFontAlign(-1, -1, 0) + .setColor(g.theme.fg) + .drawString( + "" + this.distance_to_next_point + "m", + 0, + g.getHeight() - 49 + ); + + let forward_eta = compute_eta( + now.getHours(), + now.getMinutes(), + approximate_speed, + remaining_distance / 1000 + ); + + // now display ETA + g.setFont("6x8:2") + .setFontAlign(-1, -1, 0) + .setColor(g.theme.fg) + .drawString(forward_eta, 0, 42); + + // display distance on path + g.setFont("6x8:2").drawString( + "" + rounded_distance + "/" + total, + 0, + g.getHeight() - 32 + ); + + // display various indicators + if (this.distance_to_next_point <= 100) { + if (this.path.is_waypoint(this.reaching)) { + g.setColor(0.0, 1.0, 0.0) + .setFont("6x15") + .drawString("turn", g.getWidth() - 50, 30); + } + } + } + display_path() { + // don't display all segments, only those neighbouring current segment + // this is most likely to be the correct display + // while lowering the cost a lot + // + // note that all code is inlined here to speed things up + let cos = this.adjusted_cos_direction; + let sin = this.adjusted_sin_direction; + let displayed_x = this.displayed_position.lon; + let displayed_y = this.displayed_position.lat; + let width = g.getWidth(); + let height = g.getHeight(); + let half_width = width / 2; + let half_height = height / 2 + Y_OFFSET; + let scale_factor = this.scale_factor; + if (!zoomed) { + scale_factor /= 2; + } + + if (this.path !== null) { + // compute coordinate for projection on path + let tx = (this.projected_point.lon - displayed_x) * scale_factor; + let ty = (this.projected_point.lat - displayed_y) * scale_factor; + let rotated_x = tx * cos - ty * sin; + let rotated_y = tx * sin + ty * cos; + let projected_x = half_width - Math.round(rotated_x); // x is inverted + let projected_y = half_height + Math.round(rotated_y); + + // display direction to next point if lost + if (!this.on_path) { + let next_point = this.path.point(this.current_segment + 1); + let previous_point = this.path.point(this.current_segment); + let nearest_point; + if ( + previous_point.fake_distance(this.position) < + next_point.fake_distance(this.position) + ) { + nearest_point = previous_point; + } else { + nearest_point = next_point; + } + let tx = (nearest_point.lon - displayed_x) * scale_factor; + let ty = (nearest_point.lat - displayed_y) * scale_factor; + let rotated_x = tx * cos - ty * sin; + let rotated_y = tx * sin + ty * cos; + let x = half_width - Math.round(rotated_x); // x is inverted + let y = half_height + Math.round(rotated_y); + g.setColor(1, 0, 1).drawLine(half_width, half_height, x, y); + } + + // display current-segment's projection + g.setColor(0, 0, 0); + g.fillCircle(projected_x, projected_y, 4); + } + + // now display ourselves + if (this.on_path) { + g.setColor(0, 0, 0); + } else { + g.setColor(1, 0, 1); + } + g.fillCircle(half_width, half_height, 5); + } } function load_gps(filename) { - // let's display splash screen while loading file + // let's display splash screen while loading file - let splashscreen = require("heatshrink").decompress( - atob( - "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" - ) - ); + let splashscreen = require("heatshrink").decompress( + atob( + "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" + ) + ); - g.clear(); + g.clear(); - g.drawImage(splashscreen, 0, 0); - g.setFont("6x8:2") - .setFontAlign(-1, -1, 0) - .setColor(0xf800) - .drawString(filename, 0, g.getHeight() - 30); - g.flip(); + g.drawImage(splashscreen, 0, 0); + g.setFont("6x8:2") + .setFontAlign(-1, -1, 0) + .setColor(0xf800) + .drawString(filename, 0, g.getHeight() - 30); + g.flip(); - let buffer = s.readArrayBuffer(filename); - let file_size = buffer.length; - let offset = 0; + let buffer = s.readArrayBuffer(filename); + let file_size = buffer.length; + let offset = 0; - let path = null; - let maps = []; - let interests = null; - while (offset < file_size) { - let block_type = Uint8Array(buffer, offset, 1)[0]; - offset += 1; - if (block_type == 0) { - // it's a map - console.log("loading map"); - let res = new Map(buffer, offset, filename); - let map = res[0]; - offset = res[1]; - maps.push(map); - } else if (block_type == 2) { - console.log("loading path"); - let res = new Path(buffer, offset); - path = res[0]; - offset = res[1]; - } else if (block_type == 3) { - console.log("loading interests"); - let res = new Interests(buffer, offset); - interests = res[0]; - offset = res[1]; - } else { - console.log("todo : block type", block_type); - } - } - - // checksum file size - if (offset != file_size) { - console.log("invalid file size", file_size, "expected", offset); - let msg = "invalid file\nsize " + file_size + "\ninstead of" + offset; - E.showAlert(msg).then(function() { - E.showAlert(); - start_gipy(path, maps, interests); - }); + let path = null; + let heights = null; + let maps = []; + let interests = null; + while (offset < file_size) { + let block_type = Uint8Array(buffer, offset, 1)[0]; + offset += 1; + if (block_type == 0) { + // it's a map + console.log("loading map"); + let res = new Map(buffer, offset, filename); + let map = res[0]; + offset = res[1]; + maps.push(map); + } else if (block_type == 2) { + console.log("loading path"); + let res = new Path(buffer, offset); + path = res[0]; + offset = res[1]; + } else if (block_type == 3) { + console.log("loading interests"); + let res = new Interests(buffer, offset); + interests = res[0]; + offset = res[1]; + } else if (block_type == 4) { + console.log("loading heights"); + let heights_number = path.points.length / 2; + heights = Int16Array(buffer, offset, heights_number); + offset += 2 * heights_number; } else { - start_gipy(path, maps, interests); + console.log("todo : block type", block_type); } + } + + // checksum file size + if (offset != file_size) { + console.log("invalid file size", file_size, "expected", offset); + let msg = "invalid file\nsize " + file_size + "\ninstead of" + offset; + E.showAlert(msg).then(function () { + E.showAlert(); + start_gipy(path, maps, interests, heights); + }); + } else { + start_gipy(path, maps, interests, heights); + } } class Path { - constructor(buffer, offset) { - // let p = Uint16Array(buffer, offset, 1); - // console.log(p); - let points_number = Uint16Array(buffer, offset, 1)[0]; - offset += 2; + constructor(buffer, offset) { + // let p = Uint16Array(buffer, offset, 1); + // console.log(p); + let points_number = Uint16Array(buffer, offset, 1)[0]; + offset += 2; - // path points - this.points = Float64Array(buffer, offset, points_number * 2); - offset += 8 * points_number * 2; + // path points + this.points = Float64Array(buffer, offset, points_number * 2); + offset += 8 * points_number * 2; - // path waypoints - let waypoints_len = Math.ceil(points_number / 8.0); - this.waypoints = Uint8Array(buffer, offset, waypoints_len); - offset += waypoints_len; + // path waypoints + let waypoints_len = Math.ceil(points_number / 8.0); + this.waypoints = Uint8Array(buffer, offset, waypoints_len); + offset += waypoints_len; - return [this, offset]; + return [this, offset]; + } + + is_waypoint(point_index) { + let i = Math.floor(point_index / 8); + let subindex = point_index % 8; + let r = this.waypoints[i] & (1 << subindex); + return r != 0; + } + + // return point at given index + point(index) { + let lon = this.points[2 * index]; + let lat = this.points[2 * index + 1]; + return new Point(lon, lat); + } + + // return index of segment which is nearest from point. + // we need a direction because we need there is an ambiguity + // for overlapping segments which are taken once to go and once to come back. + // (in the other direction). + nearest_segment(point, start, end, cos_direction, sin_direction) { + // we are going to compute two min distances, one for each direction. + let indices = [0, 0]; + let mins = [Number.MAX_VALUE, Number.MAX_VALUE]; + + let p1 = new Point(this.points[2 * start], this.points[2 * start + 1]); + for (let i = start + 1; i < end + 1; i++) { + let p2 = new Point(this.points[2 * i], this.points[2 * i + 1]); + + let closest_point = point.closest_segment_point(p1, p2); + let distance = point.length_squared(closest_point); + + let dot = + cos_direction * (p2.lon - p1.lon) + sin_direction * (p2.lat - p1.lat); + let orientation = +(dot < 0); // index 0 is good orientation + if (distance <= mins[orientation]) { + mins[orientation] = distance; + indices[orientation] = i - 1; + } + + p1 = p2; } - is_waypoint(point_index) { - let i = Math.floor(point_index / 8); - let subindex = point_index % 8; - let r = this.waypoints[i] & (1 << subindex); - return r != 0; - } - - // return point at given index - point(index) { - let lon = this.points[2 * index]; - let lat = this.points[2 * index + 1]; - return new Point(lon, lat); - } - - // return index of segment which is nearest from point. - // we need a direction because we need there is an ambiguity - // for overlapping segments which are taken once to go and once to come back. - // (in the other direction). - nearest_segment(point, start, end, cos_direction, sin_direction) { - // we are going to compute two min distances, one for each direction. - let indices = [0, 0]; - let mins = [Number.MAX_VALUE, Number.MAX_VALUE]; - - let p1 = new Point(this.points[2 * start], this.points[2 * start + 1]); - for (let i = start + 1; i < end + 1; i++) { - let p2 = new Point(this.points[2 * i], this.points[2 * i + 1]); - - let closest_point = point.closest_segment_point(p1, p2); - let distance = point.length_squared(closest_point); - - let dot = - cos_direction * (p2.lon - p1.lon) + sin_direction * (p2.lat - p1.lat); - let orientation = +(dot < 0); // index 0 is good orientation - if (distance <= mins[orientation]) { - mins[orientation] = distance; - indices[orientation] = i - 1; - } - - p1 = p2; - } - - // by default correct orientation (0) wins - // but if other one is really closer, return other one - if (mins[1] < mins[0] / 100.0) { - return [1, indices[1]]; - } else { - return [0, indices[0]]; - } - } - get len() { - return this.points.length / 2; + // by default correct orientation (0) wins + // but if other one is really closer, return other one + if (mins[1] < mins[0] / 100.0) { + return [1, indices[1]]; + } else { + return [0, indices[0]]; } + } + get len() { + return this.points.length / 2; + } } class Point { - constructor(lon, lat) { - this.lon = lon; - this.lat = lat; - } - coordinates(current_position, cos_direction, sin_direction, scale_factor) { - let translated = this.minus(current_position).times(scale_factor); - let rotated_x = - translated.lon * cos_direction - translated.lat * sin_direction; - let rotated_y = - translated.lon * sin_direction + translated.lat * cos_direction; - return [ - g.getWidth() / 2 - Math.round(rotated_x), // x is inverted - g.getHeight() / 2 + Math.round(rotated_y) + Y_OFFSET, - ]; - } - minus(other_point) { - let xdiff = this.lon - other_point.lon; - let ydiff = this.lat - other_point.lat; - return new Point(xdiff, ydiff); - } - plus(other_point) { - return new Point(this.lon + other_point.lon, this.lat + other_point.lat); - } - length_squared(other_point) { - let londiff = this.lon - other_point.lon; - let latdiff = this.lat - other_point.lat; - return londiff * londiff + latdiff * latdiff; - } - times(scalar) { - return new Point(this.lon * scalar, this.lat * scalar); - } - // dot(other_point) { - // return this.lon * other_point.lon + this.lat * other_point.lat; - // } - distance(other_point) { - //see https://www.movable-type.co.uk/scripts/latlong.html - const R = 6371e3; // metres - const phi1 = (this.lat * Math.PI) / 180; - const phi2 = (other_point.lat * Math.PI) / 180; - const deltaphi = ((other_point.lat - this.lat) * Math.PI) / 180; - const deltalambda = ((other_point.lon - this.lon) * Math.PI) / 180; + constructor(lon, lat) { + this.lon = lon; + this.lat = lat; + } + coordinates(current_position, cos_direction, sin_direction, scale_factor) { + let translated = this.minus(current_position).times(scale_factor); + let rotated_x = + translated.lon * cos_direction - translated.lat * sin_direction; + let rotated_y = + translated.lon * sin_direction + translated.lat * cos_direction; + return [ + g.getWidth() / 2 - Math.round(rotated_x), // x is inverted + g.getHeight() / 2 + Math.round(rotated_y) + Y_OFFSET, + ]; + } + minus(other_point) { + let xdiff = this.lon - other_point.lon; + let ydiff = this.lat - other_point.lat; + return new Point(xdiff, ydiff); + } + plus(other_point) { + return new Point(this.lon + other_point.lon, this.lat + other_point.lat); + } + length_squared(other_point) { + let londiff = this.lon - other_point.lon; + let latdiff = this.lat - other_point.lat; + return londiff * londiff + latdiff * latdiff; + } + times(scalar) { + return new Point(this.lon * scalar, this.lat * scalar); + } + // dot(other_point) { + // return this.lon * other_point.lon + this.lat * other_point.lat; + // } + distance(other_point) { + //see https://www.movable-type.co.uk/scripts/latlong.html + const R = 6371e3; // metres + const phi1 = (this.lat * Math.PI) / 180; + const phi2 = (other_point.lat * Math.PI) / 180; + const deltaphi = ((other_point.lat - this.lat) * Math.PI) / 180; + const deltalambda = ((other_point.lon - this.lon) * Math.PI) / 180; - const a = - Math.sin(deltaphi / 2) * Math.sin(deltaphi / 2) + - Math.cos(phi1) * - Math.cos(phi2) * - Math.sin(deltalambda / 2) * - Math.sin(deltalambda / 2); - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + const a = + Math.sin(deltaphi / 2) * Math.sin(deltaphi / 2) + + Math.cos(phi1) * + Math.cos(phi2) * + Math.sin(deltalambda / 2) * + Math.sin(deltalambda / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - return R * c; // in meters + return R * c; // in meters + } + fake_distance(other_point) { + return Math.sqrt(this.length_squared(other_point)); + } + // return closest point from 'this' on [v,w] segment. + // since this function is critical we inline all code here. + closest_segment_point(v, w) { + // from : https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment + // Return minimum distance between line segment vw and point p + let segment_londiff = w.lon - v.lon; + let segment_latdiff = w.lat - v.lat; + let l2 = + segment_londiff * segment_londiff + segment_latdiff * segment_latdiff; // i.e. |w-v|^2 - avoid a sqrt + if (l2 == 0.0) { + return v; // v == w case } - fake_distance(other_point) { - return Math.sqrt(this.length_squared(other_point)); - } - // return closest point from 'this' on [v,w] segment. - // since this function is critical we inline all code here. - closest_segment_point(v, w) { - // from : https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment - // Return minimum distance between line segment vw and point p - let segment_londiff = w.lon - v.lon; - let segment_latdiff = w.lat - v.lat; - let l2 = - segment_londiff * segment_londiff + segment_latdiff * segment_latdiff; // i.e. |w-v|^2 - avoid a sqrt - if (l2 == 0.0) { - return v; // v == w case - } - // Consider the line extending the segment, parameterized as v + t (w - v). - // We find projection of point p onto the line. - // It falls where t = [(p-v) . (w-v)] / |w-v|^2 - // We clamp t from [0,1] to handle points outside the segment vw. + // Consider the line extending the segment, parameterized as v + t (w - v). + // We find projection of point p onto the line. + // It falls where t = [(p-v) . (w-v)] / |w-v|^2 + // We clamp t from [0,1] to handle points outside the segment vw. - // let t = Math.max(0, Math.min(1, this.minus(v).dot(w.minus(v)) / l2)); //inlined below - let start_londiff = this.lon - v.lon; - let start_latdiff = this.lat - v.lat; - let t = - (start_londiff * segment_londiff + start_latdiff * segment_latdiff) / l2; - if (t < 0) { - t = 0; - } else { - if (t > 1) { - t = 1; - } - } - let lon = v.lon + segment_londiff * t; - let lat = v.lat + segment_latdiff * t; - return new Point(lon, lat); + // let t = Math.max(0, Math.min(1, this.minus(v).dot(w.minus(v)) / l2)); //inlined below + let start_londiff = this.lon - v.lon; + let start_latdiff = this.lat - v.lat; + let t = + (start_londiff * segment_londiff + start_latdiff * segment_latdiff) / l2; + if (t < 0) { + t = 0; + } else { + if (t > 1) { + t = 1; + } } + let lon = v.lon + segment_londiff * t; + let lat = v.lat + segment_latdiff * t; + return new Point(lon, lat); + } } let fake_gps_point = 0; - function drawMenu() { - const menu = { - "": { - title: "choose trace" - }, - }; - var files = s.list(".gps"); - for (var i = 0; i < files.length; ++i) { - menu[files[i]] = start.bind(null, files[i]); - } - menu["Exit"] = function() { - load(); - }; - E.showMenu(menu); + const menu = { + "": { + title: "choose trace", + }, + }; + var files = s.list(".gps"); + for (var i = 0; i < files.length; ++i) { + menu[files[i]] = start.bind(null, files[i]); + } + menu["Exit"] = function () { + load(); + }; + E.showMenu(menu); } function start(fn) { - E.showMenu(); - console.log("loading", fn); + E.showMenu(); + console.log("loading", fn); - load_gps(fn); + load_gps(fn); } -function start_gipy(path, maps, interests) { - console.log("starting"); +function start_gipy(path, maps, interests, heights) { + console.log("starting"); - if (!simulated && settings.disable_bluetooth) { - NRF.sleep(); // disable bluetooth completely - } + if (!simulated && settings.disable_bluetooth) { + NRF.sleep(); // disable bluetooth completely + } - status = new Status(path, maps, interests); + status = new Status(path, maps, interests, heights); - setWatch( - function() { - status.activate(); - if (in_menu) { - return; - } - in_menu = true; - const menu = { - "": { - title: "choose action" - }, - "Go Backward": { - value: go_backwards, - format: (v) => (v ? "On" : "Off"), - onchange: (v) => { - go_backwards = v; - }, - }, - Zoom: { - value: zoomed, - format: (v) => (v ? "In" : "Out"), - onchange: (v) => { - status.invalidate_caches(); - zoomed = v; - }, - }, - /*LANG*/ - "powersaving": { - value: powersaving, - onchange: (v) => { - powersaving = v; - } - }, - "back to map": function() { - in_menu = false; - E.showMenu(); - g.clear(); - g.flip(); - if (status !== null) { - status.display(); - } - }, - }; - E.showMenu(menu); + setWatch( + function () { + status.activate(); + if (in_menu) { + return; + } + in_menu = true; + const menu = { + "": { + title: "choose action", }, - BTN1, { - repeat: true - } + "Go Backward": { + value: go_backwards, + format: (v) => (v ? "On" : "Off"), + onchange: (v) => { + go_backwards = v; + }, + }, + Zoom: { + value: zoomed, + format: (v) => (v ? "In" : "Out"), + onchange: (v) => { + status.invalidate_caches(); + zoomed = v; + }, + }, + /*LANG*/ + powersaving: { + value: powersaving, + onchange: (v) => { + powersaving = v; + }, + }, + "back to map": function () { + in_menu = false; + E.showMenu(); + g.clear(); + g.flip(); + if (status !== null) { + status.display(); + } + }, + }; + E.showMenu(menu); + }, + BTN1, + { + repeat: true, + } + ); + + if (status.path !== null) { + let start = status.path.point(0); + status.displayed_position = start; + } else { + let first_map = maps[0]; + status.displayed_position = new Point( + first_map.start_coordinates[0] + + (first_map.side * first_map.grid_size[0]) / 2, + first_map.start_coordinates[1] + + (first_map.side * first_map.grid_size[1]) / 2 ); + } + status.display(); - - if (status.path !== null) { - let start = status.path.point(0); - status.displayed_position = start; - } else { - let first_map = maps[0]; - status.displayed_position = new Point( - first_map.start_coordinates[0] + - (first_map.side * first_map.grid_size[0]) / 2, - first_map.start_coordinates[1] + - (first_map.side * first_map.grid_size[1]) / 2); + Bangle.on("touch", () => { + status.activate(); + if (in_menu) { + return; } + if (status.heights !== null) { + status.screen = (status.screen + 1) % 3; + status.display(); + } + }); + + Bangle.on("stroke", (o) => { + status.activate(); + if (in_menu) { + return; + } + // we move display according to stroke + let first_x = o.xy[0]; + let first_y = o.xy[1]; + let last_x = o.xy[o.xy.length - 2]; + let last_y = o.xy[o.xy.length - 1]; + let xdiff = last_x - first_x; + let ydiff = last_y - first_y; + + let c = status.adjusted_cos_direction; + let s = status.adjusted_sin_direction; + let rotated_x = xdiff * c - ydiff * s; + let rotated_y = xdiff * s + ydiff * c; + status.displayed_position.lon += (1.3 * rotated_x) / status.scale_factor; + status.displayed_position.lat -= (1.3 * rotated_y) / status.scale_factor; status.display(); + }); - Bangle.on("stroke", (o) => { - status.activate(); - if (in_menu) { - return; + if (simulated) { + status.starting_time = getTime(); + // let's keep the screen on in simulations + Bangle.setLCDTimeout(0); + Bangle.setLCDPower(1); + Bangle.loadWidgets(); // i don't know why i cannot load them at start : they would display on splash screen + + function simulate_gps(status) { + if (status.path === null) { + let map = status.maps[0]; + let p1 = new Point(map.start_coordinates[0], map.start_coordinates[1]); + let p2 = new Point( + map.start_coordinates[0] + map.side * map.grid_size[0], + map.start_coordinates[1] + map.side * map.grid_size[1] + ); + let pos = p1.times(1 - fake_gps_point).plus(p2.times(fake_gps_point)); + if (fake_gps_point < 1) { + fake_gps_point += 0.05; } - // we move display according to stroke - let first_x = o.xy[0]; - let first_y = o.xy[1]; - let last_x = o.xy[o.xy.length - 2]; - let last_y = o.xy[o.xy.length - 1]; - let xdiff = last_x - first_x; - let ydiff = last_y - first_y; - - let c = status.adjusted_cos_direction; - let s = status.adjusted_sin_direction; - let rotated_x = xdiff * c - ydiff * s; - let rotated_y = xdiff * s + ydiff * c; - status.displayed_position.lon += 1.3 * rotated_x / status.scale_factor; - status.displayed_position.lat -= 1.3 * rotated_y / status.scale_factor; - status.display(); - }); - - if (simulated) { - status.starting_time = getTime(); - // let's keep the screen on in simulations - Bangle.setLCDTimeout(0); - Bangle.setLCDPower(1); - Bangle.loadWidgets(); // i don't know why i cannot load them at start : they would display on splash screen - - - function simulate_gps(status) { - if (status.path === null) { - let map = status.maps[0]; - let p1 = new Point(map.start_coordinates[0], map.start_coordinates[1]); - let p2 = new Point( - map.start_coordinates[0] + map.side * map.grid_size[0], - map.start_coordinates[1] + map.side * map.grid_size[1] - ); - let pos = p1.times(1 - fake_gps_point).plus(p2.times(fake_gps_point)); - if (fake_gps_point < 1) { - fake_gps_point += 0.05; - } - status.update_position(pos); - } else { - if (fake_gps_point > status.path.len - 1 || fake_gps_point < 0) { - return; - } - let point_index = Math.floor(fake_gps_point); - if (point_index >= status.path.len / 2 - 1) { - return; - } - let p1 = status.path.point(2 * point_index); // use these to approximately follow path - let p2 = status.path.point(2 * (point_index + 1)); - //let p1 = status.path.point(point_index); // use these to strictly follow path - //let p2 = status.path.point(point_index + 1); - - let alpha = fake_gps_point - point_index; - let pos = p1.times(1 - alpha).plus(p2.times(alpha)); - - if (go_backwards) { - fake_gps_point -= 0.05; // advance simulation - } else { - fake_gps_point += 0.05; // advance simulation - } - status.update_position(pos); - } + status.update_position(pos); + } else { + if (fake_gps_point > status.path.len - 1 || fake_gps_point < 0) { + return; } + let point_index = Math.floor(fake_gps_point); + if (point_index >= status.path.len / 2 - 1) { + return; + } + let p1 = status.path.point(2 * point_index); // use these to approximately follow path + let p2 = status.path.point(2 * (point_index + 1)); + //let p1 = status.path.point(point_index); // use these to strictly follow path + //let p2 = status.path.point(point_index + 1); - setInterval(simulate_gps, 500, status); - } else { - status.activate(); + let alpha = fake_gps_point - point_index; + let pos = p1.times(1 - alpha).plus(p2.times(alpha)); - let frame = 0; - let set_coordinates = function(data) { - frame += 1; - // 0,0 coordinates are considered invalid since we sometimes receive them out of nowhere - let valid_coordinates = !isNaN(data.lat) && - !isNaN(data.lon) && - (data.lat != 0.0 || data.lon != 0.0); - if (valid_coordinates) { - if (status.starting_time === null) { - status.starting_time = getTime(); - Bangle.loadWidgets(); // load them even in simulation to eat mem - } - status.update_position(new Point(data.lon, data.lat)); - } - let gps_status_color; - if (frame % 2 == 0 || valid_coordinates) { - gps_status_color = g.theme.bg; - } else { - gps_status_color = g.theme.fg; - } - if (!in_menu) { - g.setColor(gps_status_color) - .setFont("6x8:2") - .drawString("gps", g.getWidth() - 40, 30); - } - }; - - Bangle.setGPSPower(true, "gipy"); - Bangle.on("GPS", set_coordinates); + if (go_backwards) { + fake_gps_point -= 0.2; // advance simulation + } else { + fake_gps_point += 0.2; // advance simulation + } + status.update_position(pos); + } } + + setInterval(simulate_gps, 500, status); + } else { + status.activate(); + + let frame = 0; + let set_coordinates = function (data) { + frame += 1; + // 0,0 coordinates are considered invalid since we sometimes receive them out of nowhere + let valid_coordinates = + !isNaN(data.lat) && + !isNaN(data.lon) && + (data.lat != 0.0 || data.lon != 0.0); + if (valid_coordinates) { + if (status.starting_time === null) { + status.starting_time = getTime(); + Bangle.loadWidgets(); // load them even in simulation to eat mem + } + status.update_position(new Point(data.lon, data.lat)); + } + let gps_status_color; + if (frame % 2 == 0 || valid_coordinates) { + gps_status_color = g.theme.bg; + } else { + gps_status_color = g.theme.fg; + } + if (!in_menu) { + g.setColor(gps_status_color) + .setFont("6x8:2") + .drawString("gps", g.getWidth() - 40, 30); + } + }; + + Bangle.setGPSPower(true, "gipy"); + Bangle.on("GPS", set_coordinates); + } } let files = s.list(".gps"); if (files.length <= 1) { - if (files.length == 0) { - load(); - } else { - start(files[0]); - } + if (files.length == 0) { + load(); + } else { + start(files[0]); + } } else { - drawMenu(); -} \ No newline at end of file + drawMenu(); +} diff --git a/apps/gipy/heights.png b/apps/gipy/heights.png new file mode 100644 index 000000000..07f82511b Binary files /dev/null and b/apps/gipy/heights.png differ diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json index 7dd4123f6..d6b5e1405 100644 --- a/apps/gipy/metadata.json +++ b/apps/gipy/metadata.json @@ -2,13 +2,13 @@ "id": "gipy", "name": "Gipy", "shortName": "Gipy", - "version": "0.20", + "version": "0.21", "description": "Follow gpx files using the gps. Don't get lost in your bike trips and hikes.", "allow_emulator":false, "icon": "gipy.png", "type": "app", "tags": "tool,outdoors,gps", - "screenshots": [{"url":"splash.png"}], + "screenshots": [{"url":"splash.png"}, {"url":"heights.png"}, {"url":"shot.png"}], "supports": ["BANGLEJS2"], "readme": "README.md", "interface": "interface.html", diff --git a/apps/gipy/pkg/gps.d.ts b/apps/gipy/pkg/gps.d.ts index c881052f4..e4644f74f 100644 --- a/apps/gipy/pkg/gps.d.ts +++ b/apps/gipy/pkg/gps.d.ts @@ -12,6 +12,11 @@ export function get_gps_map_svg(gps: Gps): string; export function get_polygon(gps: Gps): Float64Array; /** * @param {Gps} gps +* @returns {boolean} +*/ +export function has_heights(gps: Gps): boolean; +/** +* @param {Gps} gps * @returns {Float64Array} */ export function get_polyline(gps: Gps): Float64Array; @@ -59,6 +64,7 @@ export interface InitOutput { readonly __wbg_gps_free: (a: number) => void; readonly get_gps_map_svg: (a: number, b: number) => void; readonly get_polygon: (a: number, b: number) => void; + readonly has_heights: (a: number) => number; readonly get_polyline: (a: number, b: number) => void; readonly get_gps_content: (a: number, b: number) => void; readonly request_map: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number, n: number, o: number, p: number, q: number) => number; @@ -67,11 +73,11 @@ export interface InitOutput { readonly __wbindgen_malloc: (a: number) => number; readonly __wbindgen_realloc: (a: number, b: number, c: number) => number; readonly __wbindgen_export_2: WebAssembly.Table; - readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__heb2f4d39a212d7d1: (a: number, b: number, c: number) => void; + readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hb15c13006e54cdd7: (a: number, b: number, c: number) => void; readonly __wbindgen_add_to_stack_pointer: (a: number) => number; readonly __wbindgen_free: (a: number, b: number) => void; readonly __wbindgen_exn_store: (a: number) => void; - readonly wasm_bindgen__convert__closures__invoke2_mut__h362f82c7669db137: (a: number, b: number, c: number, d: number) => void; + readonly wasm_bindgen__convert__closures__invoke2_mut__h4d77bafb1e69a027: (a: number, b: number, c: number, d: number) => void; } export type SyncInitInput = BufferSource | WebAssembly.Module; diff --git a/apps/gipy/pkg/gps.js b/apps/gipy/pkg/gps.js index 39c2a6804..563bf6251 100644 --- a/apps/gipy/pkg/gps.js +++ b/apps/gipy/pkg/gps.js @@ -205,7 +205,7 @@ function makeMutClosure(arg0, arg1, dtor, f) { return real; } function __wbg_adapter_24(arg0, arg1, arg2) { - wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__heb2f4d39a212d7d1(arg0, arg1, addHeapObject(arg2)); + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hb15c13006e54cdd7(arg0, arg1, addHeapObject(arg2)); } function _assertClass(instance, klass) { @@ -263,6 +263,16 @@ export function get_polygon(gps) { } } +/** +* @param {Gps} gps +* @returns {boolean} +*/ +export function has_heights(gps) { + _assertClass(gps, Gps); + const ret = wasm.has_heights(gps.ptr); + return ret !== 0; +} + /** * @param {Gps} gps * @returns {Float64Array} @@ -368,8 +378,8 @@ function handleError(f, args) { wasm.__wbindgen_exn_store(addHeapObject(e)); } } -function __wbg_adapter_84(arg0, arg1, arg2, arg3) { - wasm.wasm_bindgen__convert__closures__invoke2_mut__h362f82c7669db137(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3)); +function __wbg_adapter_85(arg0, arg1, arg2, arg3) { + wasm.wasm_bindgen__convert__closures__invoke2_mut__h4d77bafb1e69a027(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3)); } /** @@ -430,9 +440,6 @@ async function load(module, imports) { function getImports() { const imports = {}; imports.wbg = {}; - imports.wbg.__wbg_log_d04343b58be82b0f = function(arg0, arg1) { - console.log(getStringFromWasm0(arg0, arg1)); - }; imports.wbg.__wbindgen_string_get = function(arg0, arg1) { const obj = getObject(arg1); const ret = typeof(obj) === 'string' ? obj : undefined; @@ -441,6 +448,9 @@ function getImports() { getInt32Memory0()[arg0 / 4 + 1] = len0; getInt32Memory0()[arg0 / 4 + 0] = ptr0; }; + imports.wbg.__wbg_log_d04343b58be82b0f = function(arg0, arg1) { + console.log(getStringFromWasm0(arg0, arg1)); + }; imports.wbg.__wbindgen_object_drop_ref = function(arg0) { takeObject(arg0); }; @@ -460,6 +470,10 @@ function getImports() { const ret = getObject(arg0).fetch(getObject(arg1)); return addHeapObject(ret); }; + imports.wbg.__wbg_newwithstrandinit_05d7180788420c40 = function() { return handleError(function (arg0, arg1, arg2) { + const ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2)); + return addHeapObject(ret); + }, arguments) }; imports.wbg.__wbg_signal_31753ac644b25fbb = function(arg0) { const ret = getObject(arg0).signal; return addHeapObject(ret); @@ -471,10 +485,6 @@ function getImports() { imports.wbg.__wbg_abort_064ae59cda5cd244 = function(arg0) { getObject(arg0).abort(); }; - imports.wbg.__wbg_newwithstrandinit_05d7180788420c40 = function() { return handleError(function (arg0, arg1, arg2) { - const ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2)); - return addHeapObject(ret); - }, arguments) }; imports.wbg.__wbg_new_2d0053ee81e4dd2a = function() { return handleError(function () { const ret = new Headers(); return addHeapObject(ret); @@ -610,7 +620,7 @@ function getImports() { const a = state0.a; state0.a = 0; try { - return __wbg_adapter_84(a, state0.b, arg0, arg1); + return __wbg_adapter_85(a, state0.b, arg0, arg1); } finally { state0.a = a; } @@ -675,8 +685,8 @@ function getImports() { const ret = wasm.memory; return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper2245 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 267, __wbg_adapter_24); + imports.wbg.__wbindgen_closure_wrapper2214 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 268, __wbg_adapter_24); return addHeapObject(ret); }; diff --git a/apps/gipy/pkg/gps_bg.wasm b/apps/gipy/pkg/gps_bg.wasm index 8e0fbc07e..7a42fb564 100644 Binary files a/apps/gipy/pkg/gps_bg.wasm and b/apps/gipy/pkg/gps_bg.wasm differ diff --git a/apps/gipy/pkg/gps_bg.wasm.d.ts b/apps/gipy/pkg/gps_bg.wasm.d.ts index b4303ee30..3b95ada78 100644 --- a/apps/gipy/pkg/gps_bg.wasm.d.ts +++ b/apps/gipy/pkg/gps_bg.wasm.d.ts @@ -4,6 +4,7 @@ export const memory: WebAssembly.Memory; export function __wbg_gps_free(a: number): void; export function get_gps_map_svg(a: number, b: number): void; export function get_polygon(a: number, b: number): void; +export function has_heights(a: number): number; export function get_polyline(a: number, b: number): void; export function get_gps_content(a: number, b: number): void; export function request_map(a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number, n: number, o: number, p: number, q: number): number; @@ -12,8 +13,8 @@ export function gps_from_area(a: number, b: number, c: number, d: number): numbe export function __wbindgen_malloc(a: number): number; export function __wbindgen_realloc(a: number, b: number, c: number): number; export const __wbindgen_export_2: WebAssembly.Table; -export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__heb2f4d39a212d7d1(a: number, b: number, c: number): void; +export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hb15c13006e54cdd7(a: number, b: number, c: number): void; export function __wbindgen_add_to_stack_pointer(a: number): number; export function __wbindgen_free(a: number, b: number): void; export function __wbindgen_exn_store(a: number): void; -export function wasm_bindgen__convert__closures__invoke2_mut__h362f82c7669db137(a: number, b: number, c: number, d: number): void; +export function wasm_bindgen__convert__closures__invoke2_mut__h4d77bafb1e69a027(a: number, b: number, c: number, d: number): void; diff --git a/apps/gipy/settings.js b/apps/gipy/settings.js index 395b1ac93..1b030f5cd 100644 --- a/apps/gipy/settings.js +++ b/apps/gipy/settings.js @@ -3,6 +3,8 @@ // Load settings var settings = Object.assign({ lost_distance: 50, + wake_up_speed: 13, + active_time: 10, buzz_on_turns: false, disable_bluetooth: true, brightness: 0.5, @@ -44,6 +46,24 @@ writeSettings(); }, }, + "wake-up speed": { + value: settings.wake_up_speed, + min: 0, + max: 30, + onchange: (v) => { + settings.wake_up_speed = v; + writeSettings(); + }, + }, + "active time": { + value: settings.active_time, + min: 5, + max: 60, + onchange: (v) => { + settings.active_time = v; + writeSettings(); + }, + }, "brightness": { value: settings.brightness, min: 0, diff --git a/apps/gipy/shot.png b/apps/gipy/shot.png new file mode 100644 index 000000000..c2ffea724 Binary files /dev/null and b/apps/gipy/shot.png differ diff --git a/apps/health/ChangeLog b/apps/health/ChangeLog index 6bc15be83..02b53c56d 100644 --- a/apps/health/ChangeLog +++ b/apps/health/ChangeLog @@ -27,3 +27,5 @@ movement graph in app is now an average, not sum fix 11pm slot for daily HRM 0.26: Implement API for activity fetching +0.27: Fix typo in daily summary graph code causing graph not to load + Fix daily summaries for 31st of the month diff --git a/apps/health/app.js b/apps/health/app.js index fdc69dd28..db21d9243 100644 --- a/apps/health/app.js +++ b/apps/health/app.js @@ -48,7 +48,7 @@ function stepsPerHour() { function stepsPerDay() { E.showMessage(/*LANG*/"Loading..."); current_selection = "stepsPerDay"; - var data = new Uint16Array(31); + var data = new Uint16Array(32); require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.steps); setButton(menuStepCount); barChart(/*LANG*/"DAY", data); @@ -72,8 +72,8 @@ function hrmPerHour() { function hrmPerDay() { E.showMessage(/*LANG*/"Loading..."); current_selection = "hrmPerDay"; - var data = new Uint16Array(31); - var cnt = new Uint8Array(31); + var data = new Uint16Array(32); + var cnt = new Uint8Array(32); require("health").readDailySummaries(new Date(), h=>{ data[h.day]+=h.bpm; if (h.bpm) cnt[h.day]++; @@ -89,7 +89,7 @@ function movementPerHour() { var data = new Uint16Array(24); var cnt = new Uint8Array(24); require("health").readDay(new Date(), h=>{ - data[h.hr]+=h.movement + data[h.hr]+=h.movement; cnt[h.hr]++; }); data.forEach((d,i)=>data[i] = d/cnt[i]); @@ -100,11 +100,11 @@ function movementPerHour() { function movementPerDay() { E.showMessage(/*LANG*/"Loading..."); current_selection = "movementPerDay"; - var data = new Uint16Array(31); - var cnt = new Uint8Array(31); + var data = new Uint16Array(32); + var cnt = new Uint8Array(32); require("health").readDailySummaries(new Date(), h=>{ - data[h.hr]+=h.movement - cnt[h.hr]++; + data[h.day]+=h.movement; + cnt[h.day]++; }); data.forEach((d,i)=>data[i] = d/cnt[i]); setButton(menuMovement); diff --git a/apps/health/metadata.json b/apps/health/metadata.json index 10c8268cb..10a146bdd 100644 --- a/apps/health/metadata.json +++ b/apps/health/metadata.json @@ -2,7 +2,7 @@ "id": "health", "name": "Health Tracking", "shortName": "Health", - "version": "0.26", + "version": "0.27", "description": "Logs health data and provides an app to view it", "icon": "app.png", "tags": "tool,system,health", diff --git a/apps/messagelist/ChangeLog b/apps/messagelist/ChangeLog index 37854d8ae..899e29cb6 100644 --- a/apps/messagelist/ChangeLog +++ b/apps/messagelist/ChangeLog @@ -1,3 +1,5 @@ 0.01: New app! 0.02: Fix music updates while app is already open -0.03: Fix invalid use of Bangle.setUI \ No newline at end of file +0.03: Fix invalid use of Bangle.setUI +0.04: Fix app crashing when new message arrives + diff --git a/apps/messagelist/app.js b/apps/messagelist/app.js index dfa7e43d4..3c140a0c4 100644 --- a/apps/messagelist/app.js +++ b/apps/messagelist/app.js @@ -24,6 +24,9 @@ RIGHT = 1, LEFT = -1, // swipe directions UP = -1, DOWN = 1; // updown directions const Layout = require("Layout"); + const debug = function() { + if (global.DEBUG_MESSAGELIST) console.log.apply(console, ['messagelist:'].concat(arguments)); + } const settings = () => require("messagegui").settings(); const fontTiny = "6x8"; // fixed size, don't use this for important things @@ -45,6 +48,7 @@ /// List of all our messages let MESSAGES; const saveMessages = function() { + debug('saveMessages()'); const noSave = ["alarm", "call", "music"]; // assume these are outdated once we close the app noSave.forEach(id => remove({id: id})); require("messages").write(MESSAGES @@ -56,6 +60,7 @@ ); }; const uiRemove = function() { + debug('uiRemove()'); if (musicTimeout) clearTimeout(musicTimeout); layout = undefined; Bangle.removeListener("message", onMessage); @@ -85,11 +90,25 @@ } const setUI = function(options, cb) { + debug('setUI(', options, cb?'':cb) delete Bangle.uiRemove; // don't clear out things when switching UI within the app options = Object.assign({mode:"custom", remove: () => uiRemove()}, options); // If options={} assume we still want `remove` to be called when leaving via fast load (so we must have 'mode:custom') Bangle.setUI(options, cb); }; + /** + * Same as calling `new Layout(layout, options)`, except Bangle.uiRemove is not called + * @param {object} layout + * @param {object} options + * @returns {Layout} + */ + const makeLayout = function(layout, options) { + const remove = Bangle.uiRemove; + delete Bangle.uiRemove; // don't clear out things when setting up new Layout + const result = new Layout(layout, options); + if (remove) Bangle.uiRemove = remove; + return result; + } const remove = function(msg) { if (msg.id==="call") call = undefined; @@ -111,6 +130,7 @@ }; const onMessage = function(type, msg) { + debug(`onMessage(${type}`, msg); if (msg.handled) return; msg.handled = true; switch(type) { @@ -135,6 +155,7 @@ Bangle.on("message", onMessage); const onCall = function(msg) { + debug('onCall(', msg); if (msg.t==="remove") { call = undefined; return exitScreen("call"); @@ -145,6 +166,7 @@ showCall(); }; const onAlarm = function(msg) { + debug('onAlarm(', msg); if (msg.t==="remove") { alarm = undefined; return exitScreen("alarm"); @@ -155,6 +177,7 @@ }; let musicTimeout; const onMusic = function(msg) { + debug('onMusic(', msg); const hadMusic = !!music; if (musicTimeout) clearTimeout(musicTimeout); musicTimeout = undefined; @@ -184,6 +207,7 @@ } }; const onMap = function(msg) { + debug('onMap(', msg); const hadMap = !!map; if (msg.t==="remove") { map = undefined; @@ -196,6 +220,7 @@ else if (active==="main" && !hadMap) showMain(); // refresh menu: add "Map" entry }; const onText = function(msg) { + debug('onText(', msg); require("messages").apply(msg, MESSAGES); const mIdx = MESSAGES.findIndex(m => m.id===msg.id); if (!MESSAGES[mIdx]) if (back==="messages") back = undefined; @@ -237,6 +262,7 @@ }; const showMap = function() { + debug('showMap()'); setActive("map"); delete map.new; let m, distance, street, target, eta; @@ -254,7 +280,7 @@ } else { target = map.body; } - let layout = new Layout({ + let layout = makeLayout({ type: "v", c: [ {type: "txt", font: fontNormal, label: target, bgCol: g.theme.bg2, col: g.theme.fg2, fillx: 1, pad: 2}, { @@ -319,6 +345,7 @@ else Bangle.musicControl(action); }; const showMusic = function() { + debug('showMusic()', music); if (active!==music) setActive("music"); if (!music) music = {track: "", artist: "", album: "", state: "pause"}; delete music.new; @@ -355,7 +382,7 @@ else if (dur) info = dur; else info = {}; - layout = new Layout({ + layout = makeLayout({ type: "v", c: [ { type: "h", fillx: 1, bgCol: g.theme.bg2, col: g.theme.fg2, c: [ @@ -442,12 +469,14 @@ let layout; const clearStuff = function() { + debug('clearStuff()'); delete Bangle.appRect; layout = undefined; setUI(); g.reset().clearRect(Bangle.appRect); }; const setActive = function(screen, args) { + debug(`setActive(${screen}`, args); clearStuff(); if (active && screen!==active) back = active; if (screen==="messages") messageNum = args; @@ -476,6 +505,7 @@ } }; const showMain = function() { + debug('showMain()'); setActive("main"); let grid = {"": {title:/*LANG*/"Messages", align: 0, back: load}}; if (call) grid[/*LANG*/"Incoming Call"] = {icon: "Phone", cb: showCall}; @@ -596,7 +626,7 @@ } l.c.push(row); } - layout = new Layout(l, {back: back}); + layout = makeLayout(l, {back: back}); layout.render(); if (B2) { @@ -640,6 +670,7 @@ }; const showSettings = function() { + debug('showSettings()'); setActive("settings"); eval(require("Storage").read("messagelist.settings.js"))(() => { setFont(); @@ -647,6 +678,7 @@ }); }; const showCall = function() { + debug('showCall()'); setActive("call"); delete call.new; Bangle.setLocked(false); @@ -678,7 +710,7 @@ ]; } - layout = new Layout({ + layout = makeLayout({ type: "v", c: [ { type: "h", fillx: 1, bgCol: g.theme.bg2, col: g.theme.fg2, c: [ @@ -722,6 +754,7 @@ }); }; const showAlarm = function() { + debug('showAlarm()'); // dismissing alarms doesn't seem to work, so this is simple */ setActive("alarm"); delete alarm.new; @@ -731,7 +764,7 @@ const w = g.getWidth()-48, lines = g.setFont(fontNormal).wrapString(alarm.title, w), title = (lines.length>2) ? lines.slice(0, 2).join("\n")+"..." : lines.join("\n"); - layout = new Layout({ + layout = makeLayout({ type: "v", c: [ { type: "h", fillx: 1, bgCol: g.theme.bg2, col: g.theme.fg2, c: [ @@ -830,6 +863,7 @@ ); }; const showMessage = function(num, bottom) { + debug(`showMessage(${num}, ${!!bottom})`); if (num<0) num = 0; if (!num) num = 0; // no number: show first if (num>=MESSAGES.length) num = MESSAGES.length-1; @@ -1093,7 +1127,7 @@ let imageCol = getImageColor(msg); if (g.setColor(imageCol).getColor()==hBg) imageCol = hCol; - layout = new Layout({ + layout = makeLayout({ type: "v", c: [ { type: "h", fillx: 1, bgCol: hBg, col: hCol, c: [ @@ -1133,6 +1167,7 @@ * Stop auto-unload timeout and buzzing, remove listeners for this function */ const clearUnreadStuff = function() { + debug('clearUnreadStuff()'); require("messages").stopBuzz(); if (unreadTimeout) clearTimeout(unreadTimeout); unreadTimeout = undefined; @@ -1208,4 +1243,4 @@ // stop buzzing, auto-close timeout on input ["touch", "drag", "swipe"].forEach(l => Bangle.on(l, clearUnreadStuff)); (B2 ? [BTN1] : [BTN1, BTN2, BTN3]).forEach(b => watches.push(setWatch(clearUnreadStuff, b, false))); -} \ No newline at end of file +} diff --git a/apps/messagelist/metadata.json b/apps/messagelist/metadata.json index 37fed5795..2f1abe11d 100644 --- a/apps/messagelist/metadata.json +++ b/apps/messagelist/metadata.json @@ -1,7 +1,7 @@ { "id": "messagelist", "name": "Message List", - "version": "0.03", + "version": "0.04", "description": "Display notifications from iOS and Gadgetbridge/Android as a list", "icon": "app.png", "type": "app", diff --git a/apps/nightwatch/ChangeLog b/apps/nightwatch/ChangeLog new file mode 100644 index 000000000..c854e9e5b --- /dev/null +++ b/apps/nightwatch/ChangeLog @@ -0,0 +1,2 @@ +1.0: first working version of App +1.1: bugfix (enable settings page) diff --git a/apps/nightwatch/README.md b/apps/nightwatch/README.md new file mode 100644 index 000000000..6d1749c5d --- /dev/null +++ b/apps/nightwatch/README.md @@ -0,0 +1,20 @@ +# The Nightwatch + +Snuggle into your sleeping bag, hang the Bangle on the tent wall +and check the screen before you fall asleep: + +![](screenshot.png) +![](screenshot2.png) + + +Reads temperature and pressure sensor. Shows current, maximal and minimal values +since the start of the app. Also show a graph of the last 20 measures. + +Swipe left/right between values. + +Screen is updated periodically, time step is configurable in settings. + + +# Creator + +[Niko Komin](https://www.laikaundfreunde.de/niko-komin/) diff --git a/apps/nightwatch/metadata.json b/apps/nightwatch/metadata.json new file mode 100644 index 000000000..4de2a0271 --- /dev/null +++ b/apps/nightwatch/metadata.json @@ -0,0 +1,18 @@ +{ + "id":"nightwatch", + "readme":"README.md", + "name":"The Nightwatch", + "shortName":"Nightwatch", + "supports" : ["BANGLEJS2"], + "icon":"nightwatch.icon.png", + "screenshots" : [ { "url":"screenshot.png","url":"screenshot2.png" } ], + "version":"1.1", + "description":"Logs sensor readings (currently T and p), show min/max and graph.", + "tags": "tools,outdoors", + "storage": [ + {"name":"nightwatch.app.js","url":"nightwatch.app.js"}, + {"name":"nightwatch.settings.js","url":"nightwatch.settings.js"}, + {"name":"nightwatch.img","url":"nightwatch.icon.js","evaluate":true} + ], + "data": [{"name":"nightwatch.json"}] +} diff --git a/apps/nightwatch/nightwatch.app.info b/apps/nightwatch/nightwatch.app.info new file mode 100644 index 000000000..36345ce26 --- /dev/null +++ b/apps/nightwatch/nightwatch.app.info @@ -0,0 +1,6 @@ +require("Storage").write("nightwatch.info",{ + "id":"nightwatch", + "name":"nightwatch", + "src":"nightwatch.app.js", + "icon":"nightwatch.icon.png" +}); \ No newline at end of file diff --git a/apps/nightwatch/nightwatch.app.js b/apps/nightwatch/nightwatch.app.js new file mode 100644 index 000000000..035307106 --- /dev/null +++ b/apps/nightwatch/nightwatch.app.js @@ -0,0 +1,175 @@ +// PTLOGGER +// MEASURES p AND T PERIODICALLY AND UPDATES MIN & MAX VALS +// DISPLAYS EITHER OF BOTH + + +var settings = Object.assign({ + dt: 5, //time interval in minutes +}, require('Storage').readJSON("nightwatch.json", true) || {}); + +let dt = settings.dt; +delete settings; + +var timerID; + +const highColor = '#35b779';//#6dcd59; +const lowColor = '#eb005c';//#3d4a89;//#482878; +const normColor = '#000000'; +const historyAmnt = 24; + + +const TData = { + ondisplay:true, + unit: '\xB0C', + accuracy: 1, + value : 100, t_value:'0:00', + values : new Array(historyAmnt), + maxval : -100, t_max:'0:00', + minval : 100, t_min:'0:00' +}; + +const PData = { + ondisplay:false, + unit: 'mbar', + accuracy: 0, + value : 0, t_value:'0:00', + values : new Array(historyAmnt), + maxval : 0, t_max:'0:00', + minval : 10000, t_min:'0:00' +}; + +function minMaxString(val,accuracy,unit,time){ + return time+' '+val.toFixed(accuracy)+unit; +// return val.toFixed(accuracy)+unit+'('+time+')'; +} + +function updateScreen() { + // what are we showing right now? + let data; + if (TData.ondisplay){data = TData;} + else {data = PData;} + + // make strings + let valueString = data.value.toFixed(data.accuracy)+data.unit; + let minString = minMaxString(data.minval, data.accuracy, data.unit, data.t_min); + let maxString = minMaxString(data.maxval, data.accuracy, data.unit, data.t_max); + + // LETS PAINT + g.clear(); + g.setFontAlign(0, 0); + + // MINUM AND MAXIMUM VALUES AND TIMES + g.setFont("Vector:18"); + g.setColor(normColor); + g.drawString(maxString, g.getWidth() / 2, 11); + g.drawString(minString, g.getWidth() / 2, g.getHeight() - 11); + + g.setColor(normColor); + + // TIME OF LAST MEASURE AND SIZE OF INTERVAL + g.setFontAlign(-1, 0); + g.drawString(data.t_value, 0, g.getHeight()/2 - 25); + g.setFontAlign(1, 0); + g.drawString('dt='+dt+'min', g.getWidth() , g.getHeight()/2 - 25); + + //////////////////////////////////////////////////////////// + // GRAPH OF MEASUREMENT HISTORY + g.setFont("Vector:16"); + const graphHeight=35; + const graphWidth=g.getWidth()-30; + const graphLocX = 15; + const graphLocY = g.getHeight() - 16 - 18 - graphHeight; + + // DRAW SOME KIND OF AXES + g.setColor(0.4,0.4,0.4); + g.drawRect(graphLocX,graphLocY,graphLocX+graphWidth,graphLocY+graphHeight); + g.drawLine(graphLocX,graphLocY+graphHeight/2,graphLocX+graphWidth,graphLocY+graphHeight/2); + g.drawLine(graphLocX+graphWidth/2,graphLocY,graphLocX+graphWidth/2,graphLocY+graphHeight); + g.drawLine(graphLocX+graphWidth/4,graphLocY,graphLocX+graphWidth/4,graphLocY+graphHeight); + g.drawLine(graphLocX+3*graphWidth/4,graphLocY,graphLocX+3*graphWidth/4,graphLocY+graphHeight); + g.setColor(normColor); + + // DRAW LINE + require("graph").drawLine(g, data.values, { + x:graphLocX, + y:graphLocY, + width:graphWidth, + height:graphHeight + }); + + let graphMax=Math.max.apply(Math,data.values); + let graphMin=Math.min.apply(Math,data.values); + g.setFontAlign(0, 0); + g.setColor(highColor); + g.drawString(graphMax.toFixed(data.accuracy), g.getWidth()/2, g.getHeight() - 16 - 18 - graphHeight); + g.setColor(lowColor); + g.drawString(graphMin.toFixed(data.accuracy), g.getWidth()/2, g.getHeight() - 16 - 18); + g.setColor(normColor); + + let historyLength = (historyAmnt*dt >= 60)?('-'+historyAmnt*dt/60+'h'):('-'+historyAmnt*dt+'"'); + + g.drawString(historyLength,25, g.getHeight() - 16 - 18 - graphHeight/2); + + //////////////////////////////////////////////////////////// + // LAST MEASURE + g.setFontAlign(0, 0); + g.setFont('Vector:36'); + g.drawString(valueString, g.getWidth() / 2, g.getHeight() / 2); + + data.ondisplay = true; +} + +function updateMinMax( data, currentValue ){ + data.values.push(currentValue); + data.values.shift(); + data.value=currentValue; + + let now = new Date(); + data.t_value = now.getHours()+':'+String(now.getMinutes()).padStart(2, '0'); + if (currentValue < data.minval){data.t_min=data.t_value;data.minval = currentValue;} + if (currentValue > data.maxval){data.t_max=data.t_value;data.maxval = currentValue;} +} + +function switchDisplay(){ + if (TData.ondisplay) {TData.ondisplay=false;PData.ondisplay=true;updateScreen();} + else {PData.ondisplay=false;TData.ondisplay=true;updateScreen();} +} + +function settingsPage(){ + Bangle.on('swipe',function (){}); + eval(require("Storage").read("nightwatch.settings.js"))(()=>load()); + Bangle.on('swipe',switchDisplay); + console.log(3); +} + +function handlePressureSensorReading(data) { + updateMinMax(TData,data.temperature); + updateMinMax(PData,data.pressure); +} + +function startup(){ + // testing in emulator + // handlePressureSensorReading({ "temperature": 28.64251302083, "pressure": 1004.66520303803, "altitude": 71.72072902749 }); + // updateScreen(); + + // ON STARTUP: + // fill current reading into data, + // before `updateMinMax` uses it + Bangle.getPressure().then(d=>{TData.value=d.temperature; + TData.values.fill(d.temperature); + PData.value=d.pressure; + PData.values.fill(d.pressure); + handlePressureSensorReading(d); + updateScreen();}); + Bangle.on('swipe',switchDisplay); + + //Bangle.on('tap',settingsPage); + + timerID = setInterval( function() { + Bangle.getPressure().then(d=>{handlePressureSensorReading(d);updateScreen();}); + }, dt * 60000); + +} + +startup(); + diff --git a/apps/nightwatch/nightwatch.icon.js b/apps/nightwatch/nightwatch.icon.js new file mode 100644 index 000000000..19b4623f0 --- /dev/null +++ b/apps/nightwatch/nightwatch.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4MA///ospETUQAgc//gFDv4FF/wFP/4FF/5PCgIFChF/AoWA/1/+YFBx/+g4EBAAPAFAIEBEgUDBQYAN/E/AgQvDDoXHDocH4wFDgf8v4RCDooAMj/4AoZcBcM8DOAgFFgJSDAqQAhA==")) diff --git a/apps/nightwatch/nightwatch.icon.png b/apps/nightwatch/nightwatch.icon.png new file mode 100644 index 000000000..bf3a3282a Binary files /dev/null and b/apps/nightwatch/nightwatch.icon.png differ diff --git a/apps/nightwatch/nightwatch.info.js b/apps/nightwatch/nightwatch.info.js new file mode 100644 index 000000000..ccbc8909a --- /dev/null +++ b/apps/nightwatch/nightwatch.info.js @@ -0,0 +1,6 @@ +require("Storage").write("nightwatch.info",{ + "id":"nightwatch", + "name":"The Nightwatch", + "src":"nightwatch.app.js", + "icon":"nightwatch.icon.png" +}); diff --git a/apps/nightwatch/nightwatch.settings.js b/apps/nightwatch/nightwatch.settings.js new file mode 100644 index 000000000..744ebd8dc --- /dev/null +++ b/apps/nightwatch/nightwatch.settings.js @@ -0,0 +1,25 @@ +(function(back) { + var FILE = "nightwatch.json"; + // Load settings + var settings = Object.assign({ + dt: 5, + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + // Show the menu + E.showMenu({ + "" : { "title" : "nightwatch" }, + "< Back" : () => back(), + 'log freq (min)': { + value: 0|settings.dt, // 0| converts undefined to 0 + min: 1, max: 60, + onchange: v => { + settings.dt = v; + writeSettings(); + } + }, + }); +}); diff --git a/apps/nightwatch/screenshot.png b/apps/nightwatch/screenshot.png new file mode 100644 index 000000000..3f524eac9 Binary files /dev/null and b/apps/nightwatch/screenshot.png differ diff --git a/apps/nightwatch/screenshot2.png b/apps/nightwatch/screenshot2.png new file mode 100644 index 000000000..6fe6faf41 Binary files /dev/null and b/apps/nightwatch/screenshot2.png differ diff --git a/apps/rebbleagenda/ChangeLog b/apps/rebbleagenda/ChangeLog new file mode 100644 index 000000000..ec66c5568 --- /dev/null +++ b/apps/rebbleagenda/ChangeLog @@ -0,0 +1 @@ +0.01: Initial version diff --git a/apps/rebbleagenda/README.md b/apps/rebbleagenda/README.md new file mode 100644 index 000000000..77afd4b48 --- /dev/null +++ b/apps/rebbleagenda/README.md @@ -0,0 +1,24 @@ +# Rebble Agenda + +Agenda app for showing upcoming events in an animated fashion. +Heavily inspired by the inbuilt agenda of the pebble time. +Switch between calendar events by swiping up or down. Click the button to exit. + +![Two events shown using the default light system theme](./screenshot_rebbleagenda_events.png) ![The last event of the agenda shown using a custom red theme](./screenshot_rebbleagenda_customtheme.png) ![An animated sun shows the day of the following events](./screenshot_rebbleagenda_sun.png) + +## Settings + +- *Use system theme* - Use the colors of the system theme. Otherwise use following colors. +- *Accent* - The color of the rightmost accent bar if not following system theme. +- *Background* - The background color to use if not following system theme. +- *Foreground* - The foreground color to use if not following system theme. + +## Notes + +- The weather icon in the top right corner is currently just showing the current weather as provided by [weather](https://github.com/espruino/BangleApps/blob/master/apps/weather/). Closest forecast to be implemented in a future release. +- Events only show as much of their title and description as can be fit on the screen, which is one and four (wrapped) lines respectively. +- Events are loaded from ```android.calendar.json```, which is read in its entirety. If you have a very busy schedule, loading may take a second or two. + +## Creator + +- [Sarah Alrøe](https://github.com/SarahAlroe), August+September 2023 diff --git a/apps/rebbleagenda/app-icon.js b/apps/rebbleagenda/app-icon.js new file mode 100644 index 000000000..d432f8179 --- /dev/null +++ b/apps/rebbleagenda/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/ADuIUCARRDhgePCKIv13YAEDoYJFAA4RJFyQvcGBYRGy4dDy4uLCJgv/DoOBDgOBF5oRLF6IeBDgIvNCJYvQDwQuNCJovRADov/F9OsAEgv/F/4v/F6OIAAgHRF/4v/F/4v/CI4AdF/6HR3YAEF/4v/F/4v/F5IAdF/4v/F/4vJGEguKGEYuMAH4A/AH4A/ADIA==")) \ No newline at end of file diff --git a/apps/rebbleagenda/app.js b/apps/rebbleagenda/app.js new file mode 100644 index 000000000..3b6eca900 --- /dev/null +++ b/apps/rebbleagenda/app.js @@ -0,0 +1,583 @@ +{ + /* Requires */ + const weather = require('weather'); + require("Font6x12").add(Graphics); + require("Font8x16").add(Graphics); + const SETTINGS_FILE = "rebbleagenda.json"; + const settings = require("Storage").readJSON(SETTINGS_FILE, 1) || {'system':true, 'bg': '#fff','fg': '#000','acc': '#0FF'}; + + /* Layout consts */ + const MARKER_SIZE = 4; + const BORDER_SIZE = 6; + const WIDGET_SIZE = 24; + const PRIMARY_OFFSET = WIDGET_SIZE + BORDER_SIZE + MARKER_SIZE - 20 / 2; + const SECONDARY_OFFSET = g.getHeight() - WIDGET_SIZE - 16 - 20; + const MARKER_POS_UPPER = Uint8Array([g.getWidth() - BORDER_SIZE - MARKER_SIZE, WIDGET_SIZE + BORDER_SIZE + MARKER_SIZE]); + const PIN_SIZE = 10; + const ACCENT_WIDTH = 2 * BORDER_SIZE + 2 * MARKER_SIZE; // �=2r, borders each side. + + const TEXT_COLOR = settings.system?g.theme.fg:settings.fg; + const BG_COLOR = settings.system?g.theme.bg:settings.bg; + const ACCENT_COLOR = settings.system?g.theme.bgH:settings.acc; + const SUN_COLOR_START = 0xF800; + const SUN_COLOR_END = 0xFFE0; + const SUN_FACE = 0x0000; + + /* Animation polygon sets*/ + const CLEAR_POLYS_1 = [ + new Uint8Array([0, 176, 0, 0, 176, 0, 176, 0, 0, 0, 0, 176]), + new Uint8Array([0, 176, 0, 0, 176, 0, 170, 7, 10, 12, 7, 168]), + new Uint8Array([0, 176, 0, 0, 176, 0, 139, 49, 41, 45, 43, 125]), + new Uint8Array([0, 176, 0, 0, 176, 0, 90, 81, 82, 86, 85, 94]), + new Uint8Array([0, 176, 0, 0, 176, 0, 91, 85, 85, 85, 85, 91]) + ]; + + const CLEAR_POLYS_2 = [ + new Uint8Array([0, 176, 176, 176, 176, 0, 176, 0, 176, 176, 0, 176]), + new Uint8Array([0, 176, 176, 176, 176, 0, 170, 7, 162, 161, 7, 168]), + new Uint8Array([0, 176, 176, 176, 176, 0, 139, 49, 130, 126, 43, 125]), + new Uint8Array([0, 176, 176, 176, 176, 0, 90, 81, 95, 89, 85, 94]), + new Uint8Array([0, 176, 176, 176, 176, 0, 91, 85, 91, 91, 85, 91]) + ]; + + const BREATHING_POLYS = [ + new Uint8Array([72, 88, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 84, 88]), + new Uint8Array([63, 88, 64, 73, 78, 73, 78, 73, 78, 73, 78, 73, 92, 73, 93, 88]), + new Uint8Array([60, 88, 56, 76, 78, 60, 78, 60, 78, 60, 78, 60, 100, 76, 96, 88]), + new Uint8Array([56, 88, 50, 78, 64, 54, 78, 54, 78, 54, 92, 54, 106, 78, 100, 88]), + new Uint8Array([53, 88, 47, 80, 52, 53, 78, 41, 78, 41, 104, 53, 109, 80, 103, 88]), + new Uint8Array([50, 88, 43, 81, 43, 51, 63, 32, 92, 32, 113, 51, 113, 81, 106, 88])]; + const SUN_EYE_LEFT_POLY = new Uint8Array([56, 52, 64, 44, 72, 52, 72, 55, 69, 54, 64, 50, 58, 55, 56, 55]); + const SUN_EYE_RIGHT_OFFSET = 30; + const MOUTH_POLY = new Uint8Array([78, 77, 68, 75, 67, 73, 69, 71, 78, 73, 87, 71, 89, 73, 88, 75]); + + /* Animation timings */ + const TIME_CLEAR_ANIM = 400; + const TIME_CLEAR_BREAK = 10; + const TIME_DEFAULT_ANIM = 300; + const TIME_BUMP_ANIM = 200; + const TIME_EXIT_ANIM = 500; + const TIME_EVENT_CHANGE = 150; + const TIME_EVENT_BREAK_IN = 300; + const TIME_EVENT_BREAK_ANIM = 800; + const TIME_EVENT_BREAK_HALT = 500; + const TIME_EVENT_BREAK_OUT = 500; + + /* Utility functions */ + + /** + * Check if two dates occur on the same day + * @param {Date} d1 The first date to compare + * @param {Date} d2 The second date to compare + * @returns {Boolean} The two dates are on the same day + */ + const isSameDay = function (d1, d2) { + return (d1.getDate() == d2.getDate() && d1.getMonth() == d2.getMonth() && d1.getFullYear() == d2.getFullYear()); + }; + + /** + * Apply sinusoidal easing to a value 0-1 + * @param {Number} x Number to ease + * @returns {Number} Ease of x + */ + const ease = function (x) { + "jit"; + return 1 - (Math.cos(Math.PI * x) + 1) / 2; + }; + + /** + * Map from 0-1 to a number interval + * @param {Number} outMin Minimum output number + * @param {Number} outMax Maximum output number + * @param {Number} x Number between 0 and 1 to map from + * @returns {Number} x mapped between min and max + */ + const map = function (outMin, outMax, x) { + "jit"; + return outMin + x * (outMax - outMin); + }; + + /** + * Return [0-1] progress through an interval + * @param {Number} start When the interval was started in ms + * @param {Number} end When the interval is supposed to stop in ms + * @returns {Number} Value between 0 and 1 reflecting progress through interval + */ + const timeProgress = function (start, end) { + "jit"; + const length = end - start; + const delta = Date.now() - start; + return Math.min(Math.max(delta / length, 0), 1); + }; + + /** + * Interpolate between sets of polygon coordinates + * @param {Array} polys An array of arrays, each containing an equally long set of coordinates + * @param {Number} pos Progress through interpolation [0-1] + * @returns {Array} Interpolation between the two closest sets of coordinates + */ + const interpolatePoly = function (polys, pos) { + const span = polys.length - 1; + pos = pos * span; + pos = pos > span ? span : pos; + const upper = polys[Math.ceil(pos)]; + const lower = polys[Math.floor(Math.max(pos - 0.000001, 0))]; + const interp = pos - Math.floor(pos - 0.000001); + return upper.map((up, i) => { + return Math.round(up * interp + lower[i] * (1 - interp)); + }); + }; + + /** + * Repeatedly call callback with progress through an interval of length time + * @param {Function} anim Callback which takes i, animation progress [0-1] + * @param {Number} time How many ms the animation should last + * @returns {void} + */ + const doAnim = function (anim, time) { + const animStart = Date.now(); + const animEnd = animStart + time; + let i = 0; + do { + i = timeProgress(animStart, animEnd); + anim(i); + } while (i < 1); + anim(1); + }; + + /* Screen draw functions */ + + /** + * Draw an event + * @param {Number} index Index in the events array of event to draw + * @param {Number} yOffset Vertical pixel offset of the draw + * @param {Boolean} drawSecondary Should secondary event be drawn if possible? + */ + const drawEvent = function (index, yOffset, drawSecondary) { + g.setColor(TEXT_COLOR); + // Draw the event time + g.setFontAlign(-1, -1, 0); + g.setFont("Vector", 20); + g.drawString(events[index].time, BORDER_SIZE, PRIMARY_OFFSET + yOffset); + + // Draw the event title + g.setFont("8x16"); + g.drawString(events[index].title, BORDER_SIZE, PRIMARY_OFFSET + 20 + yOffset); + + // And the event description + g.setFont("6x12"); + g.drawString(events[index].description, BORDER_SIZE, PRIMARY_OFFSET + 20 + 12 + 2 + yOffset); + + // Draw a secondary event if asked to and exists + if (drawSecondary) { + if (index + 1 < events.length) { + if (events[index].date != events[index + 1].date) { + // If event belongs to another day, draw circle + g.fillCircle((g.getWidth() - ACCENT_WIDTH) / 2, g.getHeight() - MARKER_SIZE - WIDGET_SIZE - BORDER_SIZE + yOffset, MARKER_SIZE); + } else { + // Draw event time and title + g.setFont("Vector", 20); + g.drawString(events[index + 1].time, BORDER_SIZE, SECONDARY_OFFSET + yOffset); + g.setFont("8x16"); + g.drawString(events[index + 1].title, BORDER_SIZE, SECONDARY_OFFSET + 20 + yOffset); + } + } else { + // If no more events exist, draw end + g.setFontAlign(0, 1, 0); + g.setFont("Vector", 20); + g.drawString("End", (g.getWidth() - ACCENT_WIDTH) / 2, g.getHeight() - BORDER_SIZE + yOffset); + } + } + }; + + /** + * Draw a two-line caption beneath a figure (Just beneath centre) + * @param {String} first Top string to draw + * @param {String} second Bottom string to draw + * @param {Number} yOffset Vertical pixel offset of the draw + */ + const drawFigureCaption = function (first, second, yOffset) { + g.setFontAlign(0, -1, 0); + g.setFont("Vector", 18); + g.setColor(TEXT_COLOR); + g.drawString(first, (g.getWidth() - ACCENT_WIDTH) / 2, g.getHeight() / 2 + BORDER_SIZE + yOffset); + g.drawString(second, (g.getWidth() - ACCENT_WIDTH) / 2, g.getHeight() / 2 + BORDER_SIZE + 20 + yOffset); + }; + + /** + * Clear the contents area of the default layout + */ + const clearContent = function () { + g.setColor(BG_COLOR); + g.fillRect(0, 0, g.getWidth() - ACCENT_WIDTH - PIN_SIZE, g.getHeight()); + }; + + /** + * Draw the sun figure (above centre, in content area) + * @param {Number} progress Progress through the sun expansion animation, between 0 and 1 + * @param {Number} yOffset Vertical pixel offset of the draw + */ + const drawSun = function (progress, yOffset) { + const p = ease(progress); + const sunColor = progress == 1 ? SUN_COLOR_END : g.blendColor(SUN_COLOR_START, SUN_COLOR_END, p); + g.setColor(sunColor); + g.fillPoly(g.transformVertices(interpolatePoly(BREATHING_POLYS, p), { y: yOffset })); + + if (progress > 0.6) { + const faceP = ease((progress - 0.6) * 2.5); + g.setColor(g.blendColor(sunColor, SUN_FACE, faceP)); + g.fillPoly(g.transformVertices(SUN_EYE_LEFT_POLY, { y: map(20, 0, faceP) + yOffset })); + g.fillPoly(g.transformVertices(SUN_EYE_LEFT_POLY, { x: SUN_EYE_RIGHT_OFFSET, y: map(20, 0, faceP) + yOffset })); + g.fillPoly(g.transformVertices(MOUTH_POLY, { y: map(10, 0, faceP) + yOffset })); + } + + g.setColor(TEXT_COLOR); + g.fillRect({ + x: map((g.getWidth() - ACCENT_WIDTH) / 2 - MARKER_SIZE, 20, p), + y: map(g.getHeight() / 2 - MARKER_SIZE, g.getHeight() / 2 - MARKER_SIZE / 2, p) + yOffset, + x2: map((g.getWidth() - ACCENT_WIDTH) / 2 + MARKER_SIZE, (g.getWidth() - ACCENT_WIDTH) - 20, p), + y2: map(g.getHeight() / 2 + MARKER_SIZE / 2, g.getHeight() / 2, p) + yOffset + }); + }; + + /* Animation functions */ + + /** + * Animate clearing the screen to accent color with a single dot in the middle + */ + const animClearScreen = function () { + let oldPoly1 = CLEAR_POLYS_1[0]; + let oldPoly2 = CLEAR_POLYS_2[0]; + doAnim(i => { + i = ease(i); + poly1 = interpolatePoly(CLEAR_POLYS_1, i); + poly2 = interpolatePoly(CLEAR_POLYS_2, i); + // Fill in black line + g.setColor(TEXT_COLOR); + g.fillPoly(poly1); + g.fillPoly(poly2); + + // Fill in outer shape + g.setColor(ACCENT_COLOR); + g.fillPoly(oldPoly1); + g.fillPoly(oldPoly2); + g.flip(); + + // Save poly for next loop outer shape + oldPoly1 = poly1; + oldPoly2 = poly2; + }, TIME_CLEAR_ANIM); + + // Draw circle + g.setColor(TEXT_COLOR); + g.fillCircle(g.getWidth() / 2, g.getHeight() / 2, MARKER_SIZE); + g.flip(); + }; + + /** + * Animate from a cleared screen and dot to the default layout + */ + const animDefaultScreen = function () { + doAnim(i => { + // Draw the circle moving into the corner + i = ease(i); + const circleX = map(g.getWidth() / 2, MARKER_POS_UPPER[0], i); + const circleY = map(g.getHeight() / 2, MARKER_POS_UPPER[1], i); + g.setColor(TEXT_COLOR); + g.fillCircle(circleX, circleY, MARKER_SIZE); + + // Move the background poly in from the left + g.setColor(BG_COLOR); + const accentX = map(0, g.getWidth() - ACCENT_WIDTH, i); + g.fillPoly([0, 0, accentX, 0, accentX, MARKER_POS_UPPER[1] - PIN_SIZE, accentX - PIN_SIZE, MARKER_POS_UPPER[1], accentX, MARKER_POS_UPPER[1] + PIN_SIZE, accentX, 176, 0, 176]); + g.flip(); + + // Clear the circle for the next loop + g.setColor(ACCENT_COLOR); + g.fillCircle(circleX, circleY, MARKER_SIZE + 2); + }, TIME_DEFAULT_ANIM); + + // Finish up the circle + const w = weather.get(); + if (w && (w.code || w.txt)) { + doAnim(i => { + weather.drawIcon(w, MARKER_POS_UPPER[0], MARKER_POS_UPPER[1], MARKER_SIZE * 2); + g.setColor(TEXT_COLOR); + g.fillCircle(MARKER_POS_UPPER[0], MARKER_POS_UPPER[1], MARKER_SIZE * ease(1 - i)); + g.flip(); + }, 100); + } else { + g.setColor(TEXT_COLOR); + g.fillCircle(MARKER_POS_UPPER[0], MARKER_POS_UPPER[1], MARKER_SIZE); + } + }; + + /** + * Animate the sun figure expand or shrink fully + * @param {Number} direction Direction in which to animate. +1 = Expand. -1 = Shrink + */ + const animSun = function (direction) { + doAnim(i => { + // Clear and redraw just the sun area + g.setColor(BG_COLOR); + g.fillRect(0, 31, g.getWidth() - ACCENT_WIDTH - PIN_SIZE, g.getHeight() / 2 + 4); + drawSun((direction == 1 ? 0 : 1) + i * direction, 0); + g.flip(); + }, TIME_EVENT_BREAK_ANIM); + }; + + /** + * Animate from centre dot to an event or backwards. Used for entering (forwards) or leaving (backwards) the day-change animation + * @param {Number} index Index of the event to draw animate in or out + * @param {Number} direction Direction of the animation. +1 = Event -> Dot. -1 = Dot -> Event + */ + const animEventToMarker = function (index, direction) { + doAnim(i => { + let ei = direction == 1 ? ease(i) : ease(1 - i); + clearContent(); + drawEvent(index, -(SECONDARY_OFFSET - PRIMARY_OFFSET) * ei, false); + g.fillCircle((g.getWidth() - ACCENT_WIDTH) / 2, map(g.getHeight() - MARKER_SIZE - WIDGET_SIZE - BORDER_SIZE, g.getHeight() / 2, ei), MARKER_SIZE); + g.flip(); + }, TIME_EVENT_BREAK_IN); + + }; + + /** + * Blit the current contents of content area out of screen, replacing it with something. Currently only for moving stuff upwards. + * @param {Function} thing Callback for the new thing to draw on the screen + * @param {Number} time How long the animation should last + */ + const animBlitToX = function (thing, time) { + let oldI = 0; + doAnim(i => { + // Move stuff out of frame, index into frame + g.blit({ + x1: 0, + y1: 0, + w: g.getWidth() - ACCENT_WIDTH - PIN_SIZE, + h: ease(1 - oldI) * g.getHeight(), + x2: 0, + y2: - (ease(i) - ease(oldI)) * g.getHeight(), + setModified: true + }); + g.setColor(BG_COLOR); + // Only clear where old stuff no longer is + g.fillRect(0, g.getHeight() * (1 - ease(i)), g.getWidth() - ACCENT_WIDTH - PIN_SIZE, g.getHeight()); + thing(i); + g.flip(); + oldI = i; + }, time); + }; + + /** + * Transition between one event and another, showing a day-change animation if needed + * @param {Number} startIndex The event index that we are animating out of + * @param {Number} endIndex The event index that we are animating into + */ + const animEventTransition = function (startIndex, endIndex) { + if (events[startIndex].date == events[endIndex].date) { + // If both events are within the same day, just scroll from one to the other. + // First determine which event is on top and which direction we are animating in + let topIndex = (startIndex < endIndex) ? startIndex : endIndex; + let botIndex = (startIndex < endIndex) ? endIndex : startIndex; + let direction = (startIndex < endIndex) ? 1 : -1; + let offset = (startIndex < endIndex) ? 0 : 1; + + doAnim(i => { + // Animate the two events moving towards their destinations + clearContent(); + drawEvent(topIndex, -(SECONDARY_OFFSET - PRIMARY_OFFSET) * ease(offset + direction * i), false); + drawEvent(botIndex, (SECONDARY_OFFSET - PRIMARY_OFFSET) - (SECONDARY_OFFSET - PRIMARY_OFFSET) * ease(offset + direction * i), true); + g.flip(); + }, TIME_EVENT_CHANGE); + + // Finally, reset contents and redraw for good measure + clearContent(); + drawEvent(endIndex, 0, true); + g.flip(); + } else { + // The events are on different days, trigger day-change animation + if (startIndex < endIndex) { + // Destination is later, Stuff moves upwards + animEventToMarker(startIndex, 1); // The day-end dot moves to center of screen + drawFigureCaption(events[endIndex].weekday, events[endIndex].date, 0); // Caption between sun appears, no need to continuously redraw + animSun(1); // Animate the sun expanding + doAnim(i => { }, TIME_EVENT_BREAK_HALT); // Wait for a moment + animBlitToX(i => { drawEvent(endIndex, g.getHeight() - g.getHeight() * ease(i), true); }, TIME_EVENT_BREAK_OUT); // Blit the sun and caption out, replacing with destination event + } else { + // Destination is earlier, content moves downwards + doAnim(i => { + // Can't animBlit, draw sun and figure caption replacing origin event + clearContent(); + drawEvent(startIndex, g.getHeight() * ease(i), true); + drawSun(1, - g.getHeight() * ease(1 - i)); + drawFigureCaption(events[endIndex].weekday, events[endIndex].date, - g.getHeight() * ease(1 - i)); + g.flip(); + }, TIME_EVENT_BREAK_OUT); + doAnim(i => { }, TIME_EVENT_BREAK_HALT); // Wait for a moment + animSun(-1); // Collapse the sun + animEventToMarker(endIndex, -1); // Animate from dot to destination event + } + } + g.flip(); + }; + + /** + * Bump the event because we've reached an end + * @param {Number} index The index of the event which we are currently at (probably last) + * @param {Number} direction Which direction to bump. +1 = content moves down, then up. -1 = content moves up, back down + */ + const animEventBump = function (index, direction) { + doAnim(i => { + clearContent(); + drawEvent(index, Math.sin(Math.PI * i) * 24 * direction, true); + g.flip(); + }, TIME_BUMP_ANIM); + }; + + /** + * Run the exit animation of the application + */ + const animExit = function () { + // First, move out (downwards) the current event + doAnim(i => { + clearContent(); + drawEvent(currentEventIndex, ease(i) * g.getHeight(), true); + g.flip(); + }, TIME_EXIT_ANIM / 3 * 2); + + // Clear the screen leftwards with the accent color + g.setColor(ACCENT_COLOR); + doAnim(i => { + g.fillRect(ease(1 - i) * g.getWidth(), 0, g.getWidth(), g.getHeight()); + g.flip(); + }, TIME_EXIT_ANIM / 3); + }; + + /** + * Animate from empty default screen to the first event to show. + * If the event we're moving to is not later today, show the date first. + */ + const animFirstEvent = function () { + if (!isSameDay(new Date(events[currentEventIndex].timestamp * 1000), new Date())) { + drawFigureCaption(events[currentEventIndex].weekday, events[currentEventIndex].date, 0); + animSun(1); + doAnim(i => { }, TIME_EVENT_BREAK_HALT); + animBlitToX(i => { drawEvent(currentEventIndex, g.getHeight() - g.getHeight() * ease(i), true); }, TIME_EVENT_BREAK_OUT, 1); + } else { + drawEvent(currentEventIndex, 0, true); + } + }; + + /* Setup */ + + /* Load events */ + const today = new Date(); + const tomorrow = new Date(); + const yesterday = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + yesterday.setDate(yesterday.getDate() - 1); + g.setFont("6x12"); + const locale = require("locale"); + + let events = (require("Storage").readJSON("android.calendar.json", true) || []).map(event => { + // Title uses 8x16 font, 8 px wide characters. Limit title to fit on a line. + let title = event.title; + if (title.length > (g.getWidth() - 2 * BORDER_SIZE - ACCENT_WIDTH) / 8) { + title = title.slice(0, ((g.getWidth() - 2 * BORDER_SIZE - ACCENT_WIDTH) / 8) - 3) + "..."; + } + + // Wrap description to fit four lines of content + let description = g.wrapString(event.description, g.getWidth() - 2 * BORDER_SIZE - ACCENT_WIDTH - PIN_SIZE).slice(0, 4).join("\n"); + + // Set weekday text + let eventDate = new Date(event.timestamp * 1000); + let weekday = locale.dow(eventDate); + if (isSameDay(eventDate, today)) { + weekday = /*LANG*/"Today"; + } else if (isSameDay(eventDate, tomorrow)) { + weekday = /*LANG*/"Tomorrow"; + } else if (isSameDay(eventDate, yesterday)) { + weekday = /*LANG*/"Yesterday"; + } + + return { + timestamp: event.timestamp, + weekday: weekday, + date: locale.date(eventDate, 1), + time: locale.time(eventDate, 1) + locale.meridian(eventDate), + title: title, + description: description + }; + }).sort((a, b) => { return a.timestamp - b.timestamp; }); + + // If no events, add a note. + if (events.length == 0) { + events[0] = { + timestamp: Date.now() / 1000, + weekday: /*LANG*/"Today", + date: require("locale").date(new Date(), 1), + time: require("locale").time(new Date(), 1), + title: /*LANG*/"No events", + description: /*LANG*/"Nothing to do" + }; + } + + // We should start at the first event later than now + let currentEventIndex = events.findIndex((event) => { return event.timestamp * 1000 > Date.now(); }); + if (currentEventIndex == -1) currentEventIndex = 0; // Or just first event if none found + + // Setup the UI with remove to support fast load + Bangle.setUI({ + mode: "custom", + btn: () => { animExit(); Bangle.load(); }, + remove: function () { + require("widget_utils").show(); + delete Graphics.prototype.Font6x12; + delete Graphics.prototype.Font8x16; + Bangle.removeListener('swipe', onSwipe); + }, + }); + + /** + * Callback for swipe gesture. Transitions between adjacent events. + * @param {Number} directionLR Unused. + * @param {Number} directionUD Whether swipe direction is up or down + */ + const onSwipe = function (directionLR, directionUD) { + if (directionUD == -1) { + // Swiping up + if (currentEventIndex + 1 < events.length) { + // Animate to the next event + animEventTransition(currentEventIndex, currentEventIndex + 1); + currentEventIndex += 1; + } else { + // We've hit the end, bump + animEventBump(currentEventIndex, -1); + } + } else if (directionUD == 1) { + //Swiping down + if (currentEventIndex > 0) { + // Animate to the previous event + animEventTransition(currentEventIndex, currentEventIndex - 1); + currentEventIndex -= 1; + } else { + // If swiping earlier than earliest event, exit back to watchface + animExit(); + Bangle.load(); + } + } + }; + + // Ready animations for showing the first event, then register swipe listener for switching events + setTimeout(() => { + animDefaultScreen(); + animFirstEvent(); + Bangle.on('swipe', onSwipe); + }, TIME_CLEAR_ANIM + TIME_CLEAR_BREAK); + animClearScreen(); // Start visible changes by clearing the screen + + // Load and hide widgets to background + Bangle.loadWidgets(); + require("widget_utils").hide(); +} \ No newline at end of file diff --git a/apps/rebbleagenda/app.png b/apps/rebbleagenda/app.png new file mode 100644 index 000000000..207156565 Binary files /dev/null and b/apps/rebbleagenda/app.png differ diff --git a/apps/rebbleagenda/metadata.json b/apps/rebbleagenda/metadata.json new file mode 100644 index 000000000..07227d3bc --- /dev/null +++ b/apps/rebbleagenda/metadata.json @@ -0,0 +1,25 @@ +{ "id": "rebbleagenda", + "name": "Rebble Agenda", + "shortName":"Agenda", + "version":"0.01", + "description": "A pebble-inspired animated agenda", + "icon": "app.png", + "screenshots" : [ + { "url":"screenshot_rebbleagenda_events.png" }, + { "url":"screenshot_rebbleagenda_customtheme.png" }, + { "url":"screenshot_rebbleagenda_sun.png" } + ], + "type": "app", + "tags": "agenda,tool", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "dependencies" : { "weather":"app" }, + "storage": [ + {"name":"rebbleagenda.app.js","url":"app.js"}, + {"name":"rebbleagenda.settings.js","url":"settings.js"}, + {"name":"rebbleagenda.img","url":"app-icon.js","evaluate":true} + ], + "data": [ + {"name":"rebbleagenda.json"} + ] +} diff --git a/apps/rebbleagenda/screenshot_rebbleagenda_customtheme.png b/apps/rebbleagenda/screenshot_rebbleagenda_customtheme.png new file mode 100644 index 000000000..2d9959a5e Binary files /dev/null and b/apps/rebbleagenda/screenshot_rebbleagenda_customtheme.png differ diff --git a/apps/rebbleagenda/screenshot_rebbleagenda_events.png b/apps/rebbleagenda/screenshot_rebbleagenda_events.png new file mode 100644 index 000000000..c94c0d9c4 Binary files /dev/null and b/apps/rebbleagenda/screenshot_rebbleagenda_events.png differ diff --git a/apps/rebbleagenda/screenshot_rebbleagenda_sun.png b/apps/rebbleagenda/screenshot_rebbleagenda_sun.png new file mode 100644 index 000000000..16a63002b Binary files /dev/null and b/apps/rebbleagenda/screenshot_rebbleagenda_sun.png differ diff --git a/apps/rebbleagenda/settings.js b/apps/rebbleagenda/settings.js new file mode 100644 index 000000000..8ed2ceae5 --- /dev/null +++ b/apps/rebbleagenda/settings.js @@ -0,0 +1,69 @@ +(function (back) { + const SETTINGS_FILE = "rebbleagenda.json"; + + // initialize with default settings... + let s = { + 'system': true, + 'bg': "#FFF", + 'fg': "#000", + 'acc': "#0FF" + }; + + // ...and overwrite them with any saved values + // This way saved values are preserved if a new version adds more settings + const storage = require('Storage'); + let settings = storage.readJSON(SETTINGS_FILE, 1) || {}; + const saved = settings || {}; + for (const key in saved) { + s[key] = saved[key]; + } + + const save = function () { + settings = s; + storage.write(SETTINGS_FILE, settings); + }; + + const color_options = [/*LANG*/"Red", /*LANG*/"Green", /*LANG*/"Blue", /*LANG*/"Purple", /*LANG*/"Cyan", /*LANG*/"Orange", /*LANG*/"Grey"]; + const color_codes = ['#F00','#0F0','#00F','#F0F','#0FF','#FF0', "#888"]; + const ground_options = [/*LANG*/"Black", /*LANG*/"White", /*LANG*/"Dark Blue", /*LANG*/"Dark Red", /*LANG*/"Dark Green", /*LANG*/"Light Blue", /*LANG*/"Light Red", /*LANG*/"Light Green"]; + const ground_codes = ["#000", "#FFF", "#003", "#300", "#030", "#BBF", "#FBB", "#BFB"]; + + E.showMenu({ + '': { 'title': 'Rebble Agenda' }, + /*LANG*/'< Back': back, + /*LANG*/'Use system theme': { + value: !!s.system, + onchange: v => { + s.system = v; + save(); + }, + }, + /*LANG*/'Accent': { + value: 0 | color_codes.indexOf(s.acc), + min: 0, max: color_codes.length-1, + format: v => color_options[v], + onchange: v => { + s.acc = color_codes[v]; + save(); + }, + }, + /*LANG*/'Background': { + value: 0 | ground_codes.indexOf(s.bg), + min: 0, max: ground_codes.length-1, + format: v => ground_options[v], + onchange: v => { + s.bg = ground_codes[v]; + save(); + }, + }, + /*LANG*/'Foreground': { + value: 0 | ground_codes.indexOf(s.fg), + min: 0, max: ground_codes.length-1, + format: v => ground_options[v], + onchange: v => { + s.fg = ground_codes[v]; + save(); + }, + } + }); +}); \ No newline at end of file diff --git a/apps/sched/interface.html b/apps/sched/interface.html index cd2c9c595..53b443371 100644 --- a/apps/sched/interface.html +++ b/apps/sched/interface.html @@ -16,14 +16,18 @@ function readFile(input) { for(let i=0; i { - const jCalData = ICAL.parse(reader.result); + const icalText = reader.result.substring(reader.result.indexOf("BEGIN:VCALENDAR")); // remove html before data + const jCalData = ICAL.parse(icalText); const comp = new ICAL.Component(jCalData); + const vtz = comp.getFirstSubcomponent('vtimezone'); + const tz = new ICAL.Timezone(vtz); + // Fetch the VEVENT part comp.getAllSubcomponents('vevent').forEach(vevent => { event = new ICAL.Event(vevent); const exists = alarms.some(alarm => alarm.id === event.uid); - const alarm = eventToAlarm(event, offsetMinutes*60*1000); + const alarm = eventToAlarm(event, tz, offsetMinutes*60*1000); renderAlarm(alarm, exists); if (exists) { @@ -68,7 +72,8 @@ function getAlarmDefaults() { }; } -function eventToAlarm(event, offsetMs) { +function eventToAlarm(event, tz, offsetMs) { + event.startDate.zone = tz; const dateOrig = event.startDate.toJSDate(); const date = offsetMs ? new Date(dateOrig - offsetMs) : dateOrig; diff --git a/apps/sokoban/ChangeLog b/apps/sokoban/ChangeLog new file mode 100644 index 000000000..f931ec63e --- /dev/null +++ b/apps/sokoban/ChangeLog @@ -0,0 +1,5 @@ +0.01: Initial code +0.02: + * Fix for last level offsets parsing + * Fix for title display + diff --git a/apps/sokoban/README.md b/apps/sokoban/README.md new file mode 100644 index 000000000..36097d66f --- /dev/null +++ b/apps/sokoban/README.md @@ -0,0 +1,20 @@ +# Sokoban + +Classic Sokoban game. + +Tap screen at bottom/top/left/right to push boxes into their destinations. +Swipe to undo. + +![Screenshot](soko.png) + +You play the yellow disk (rice hat seen from above). +Each level has a set of crates (brown if incorrectly placed or blue if correctly placed) +and a set of placeholders (empty blue squares). Simply push all crates into their placeholders. +Remember you can push but never pull. + +## Creator + +Levels are the [Microban](http://www.abelmartin.com/rj/sokobanJS/Skinner/David%20W.%20Skinner%20-%20Sokoban.htm) levels +by David W. Skinner. + +frederic.wagner@imag.fr diff --git a/apps/sokoban/TODO b/apps/sokoban/TODO new file mode 100644 index 000000000..dcad68d38 --- /dev/null +++ b/apps/sokoban/TODO @@ -0,0 +1,2 @@ +- background +- win screen + final win screen diff --git a/apps/sokoban/app-icon.js b/apps/sokoban/app-icon.js new file mode 100644 index 000000000..e8a1d4b0f --- /dev/null +++ b/apps/sokoban/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4A/ABMN7oXV7vd6AuVAAIXaAwYAEC75fGC6KPFC58BiMQJxAXLiIABDAcBigXRAAIFDC5pGBAAwvOCQYbDiBfOLwgYBO54qER6QXDexIXJRggXRIxIXpb4wXMLwYvTdIinSC4gYFC5xiIC54YDC54SBIQZHRC4gcFC5hyEC4KPPLIrZGC5IWCLwgXPUApeGC5KfGC6DnGIwwXLB4gXQgI/FAwy/MAH4A/ABgA==")) diff --git a/apps/sokoban/app.js b/apps/sokoban/app.js new file mode 100644 index 000000000..3915556e3 --- /dev/null +++ b/apps/sokoban/app.js @@ -0,0 +1,471 @@ +// basic shapes +const SPACE = 0; +const WALL = 1; +const PLAYER = 2; +const BOX = 3; +const HOLE = 4; +const FILLED = 5; + +// basic directions +const LEFT = 0; +const UP = 1; +const DOWN = 2; +const RIGHT = 3; + +function go(line, column, direction) { + let destination_line = line; + let destination_column = column; + if (direction == LEFT) { + destination_column -= 1; + } else if (direction == RIGHT) { + destination_column += 1; + } else if (direction == UP) { + destination_line -= 1; + } else { + // direction is down + destination_line += 1; + } + return [destination_line, destination_column]; +} + +Bangle.setOptions({ + lockTimeout: 60000, + backlightTimeout: 60000, +}); + +let s = require("Storage"); + +// parse the levels a bit more to figure offsets delimiting next map. +function next_map_offsets(filename, start_offset) { + let raw_maps = s.readArrayBuffer(filename); + let offsets = []; + // this is a very dumb parser : map starts three chars after the end of a line with a ';' + // and ends two chars before next ';' + let comment_line = true; + for (let i = start_offset; i < raw_maps.length; i++) { + if (raw_maps[i] == 59) { // ';' + if (offsets.length != 0) { + offsets.push(i - 2); + return offsets; + } + comment_line = true; + } else if (raw_maps[i] == 10) { // '\n' + if (comment_line) { + comment_line = false; + offsets.push(i + 3); + } + } + } + if (offsets.length == 1) { + offsets.push(raw_maps.length); + } + return offsets; +} + +let config = s.readJSON("sokoban.json", true); +if (config === undefined) { + let initial_offsets = next_map_offsets("sokoban.microban.sok", 0); + config = { + levels_sets: ["sokoban.microban.sok"], // all known files containing levels + levels_set: 0, // which set are we using ? + current_maps: [0], // what is current map on each set ? + offsets: [initial_offsets], // known offsets for each levels set (binary positions of maps in each file) + }; + s.writeJSON("sokoban.json", config); +} + +let map = null; +let in_menu = false; +let history = null; // store history to allow undos + + +function load_map(filename, start_offset, end_offset, name) { + console.log("loading map in", filename, "between", start_offset, "and", end_offset); + let raw_map = new Uint8Array(s.readArrayBuffer(filename), start_offset, end_offset - start_offset); + let dimensions = map_dimensions(raw_map); + history = []; + return new Map(dimensions, raw_map, filename, name); +} + +function load_current_map() { + let current_set = config.levels_set; + let offsets = config.offsets[current_set]; + let set_filename = config.levels_sets[current_set]; + let set_name = set_filename.substring(8, set_filename.length - 4); // remove '.txt' and 'sokoban.' + let current_map = config.current_maps[current_set]; + map = load_map(set_filename, offsets[2 * current_map], offsets[2 * current_map + 1], set_name + " " + (current_map + 1)); + map.display(); +} + +function next_map() { + let current_set = config.levels_set; + let current_map = config.current_maps[current_set]; + let offsets = config.offsets[current_set]; + let won = false; + if (2 * (current_map + 1) >= offsets.length) { + // we parse some new offsets + let new_offsets = next_map_offsets(config.levels_sets[current_set], offsets[offsets.length - 1] + 2); // +2 since we need to start at ';' (we did -2 from ';' in previous parser call) + if (new_offsets.length != 2) { + won = true; + E.showAlert("You Win", "All levels completed").then(function() { + load(); + }); + } else { + config.offsets[current_set].push(new_offsets[0]); + config.offsets[current_set].push(new_offsets[1]); + } + } + if (!won) { + config.current_maps[current_set]++; + s.writeJSON("sokoban.json", config); + load_current_map(); + } +} + +function previous_map() { + let current_set = config.levels_set; + let current_map = config.current_maps[current_set]; + if (current_map > 0) { + current_map--; + config.current_maps[current_set] = current_map; + s.writeJSON("sokoban.json", config); + load_current_map(); + } +} + +function map_dimensions(raw_map) { + let line_start = 0; + let width = 0; + let height = 0; + for (let i = 0; i < raw_map.length; i++) { + if (raw_map[i] == 10) { + height += 1; + let line_width = i - line_start; + if (i > 0 && raw_map[i - 1] == 13) { + line_width -= 1; // remove \r + } + width = Math.max(line_width, width); + line_start = i + 1; + } + } + return [width, height]; +} + +class Map { + constructor(dimensions, raw_map, filename, name) { + this.filename = filename; + this.name = name; + this.width = dimensions[0]; + this.height = dimensions[1]; + this.remaining_holes = 0; + // start by creating an empty map + this.m = []; + for (let i = 0; i < this.height; i++) { + let line = new Uint8Array(this.width); + for (let j = 0; j < this.width; j++) { + line[j] = SPACE; + } + this.m.push(line); + } + // now fill with raw_map's content + let current_line = 0; + let line_start = 0; + for (let i = 0; i < raw_map.length; i++) { + if (raw_map[i] == 32) { + this.m[current_line][i - line_start] = SPACE; + } else if (raw_map[i] == 43) { + // '+' + this.remaining_holes += 1; + this.m[current_line][i - line_start] = HOLE; + this.player_column = i - line_start; + this.player_line = current_line; + } else if (raw_map[i] == 10) { + current_line += 1; + line_start = i + 1; + } else if (raw_map[i] == 35) { + this.m[current_line][i - line_start] = WALL; + } else if (raw_map[i] == 36) { + this.m[current_line][i - line_start] = BOX; + } else if (raw_map[i] == 46) { + this.remaining_holes += 1; + this.m[current_line][i - line_start] = HOLE; + } else if (raw_map[i] == 64) { + this.m[current_line][i - line_start] = SPACE; + this.player_column = i - line_start; + this.player_line = current_line; + } else if (raw_map[i] == 42) { + this.m[current_line][i - line_start] = FILLED; + } else if (raw_map[i] != 13) { + console.log("warning unknown map content", raw_map[i]); + } + } + this.steps = 0; + this.calibrate(); + } + // compute scale + calibrate() { + let r = Bangle.appRect; + let rwidth = 1 + r.x2 - r.x; + let rheight = 1 + r.y2 - r.y; + let cell_width = Math.floor(rwidth / this.width); + let cell_height = Math.floor(rheight / this.height); + let cell_scale = Math.min(cell_width, cell_height); // we want square cells + let real_width = this.width * cell_scale; + let real_height = this.height * cell_scale; + let sx = r.x + Math.ceil((rwidth - real_width) / 2); + let sy = r.y + Math.ceil((rheight - real_height) / 2); + this.sx = sx; + this.sy = sy; + this.cell_scale = cell_scale; + } + undo(direction, pushing) { + this.steps -= 1; + + let previous_position = go(this.player_line, this.player_column, 3 - direction); + let previous_line = previous_position[0]; + let previous_column = previous_position[1]; + + if (pushing) { + // put the box back on current player position + let currently_on = this.m[this.player_line][this.player_column]; + if (currently_on == HOLE) { + this.remaining_holes -= 1; + this.m[this.player_line][this.player_column] = FILLED; + } else { + this.m[this.player_line][this.player_column] = BOX; + } + // now, remove the box from its current position + let current_box_position = go(this.player_line, this.player_column, direction); + let box_line = current_box_position[0]; + let box_column = current_box_position[1]; + let box_on = this.m[box_line][box_column]; + if (box_on == FILLED) { + this.remaining_holes += 1; + this.m[box_line][box_column] = HOLE; + } else { + this.m[box_line][box_column] = SPACE; + } + this.display_cell(box_line, box_column); + } + // cancel player display + this.display_cell(this.player_line, this.player_column); + // re-display player at previous position + this.player_line = previous_line; + this.player_column = previous_column; + this.display_player(); + } + move(direction) { + let destination_position = go(this.player_line, this.player_column, direction); + let destination_line = destination_position[0]; + let destination_column = destination_position[1]; + let destination = this.m[destination_line][destination_column]; + let pushing = false; + if (destination == BOX || destination == SPACE || destination == HOLE || destination == FILLED) { + if (destination == BOX || destination == FILLED) { + pushing = true; + let after_line = 2 * destination_line - this.player_line; + let after_column = 2 * destination_column - this.player_column; + let after = this.m[after_line][after_column]; + let will_remain = SPACE; + if (destination == FILLED) { + will_remain = HOLE; + } + if (after == SPACE) { + if (will_remain == HOLE) { + this.remaining_holes += 1; + } + this.m[destination_line][destination_column] = will_remain; + this.m[after_line][after_column] = BOX; + } else if (after == HOLE) { + this.m[destination_line][destination_column] = will_remain; + this.m[after_line][after_column] = FILLED; + if (will_remain == SPACE) { + this.remaining_holes -= 1; + } + if (this.remaining_holes == 0) { + in_menu = true; + this.steps += 1; + E.showAlert("" + this.steps + "steps", "You Win").then(function() { + in_menu = false; + next_map(); + }); + return; + } + } else { + return; + } + this.display_cell(after_line, after_column); + this.display_cell(destination_line, destination_column); + } + history.push([direction, pushing]); + this.display_cell(this.player_line, this.player_column); + this.steps += 1; + this.player_line = destination_line; + this.player_column = destination_column; + this.display_player(); + // this.display(); + } + } + display_player() { + sx = this.sx; + sy = this.sy; + cell_scale = this.cell_scale; + g.setColor(0.8, 0.8, 0).fillCircle(sx + (0.5 + this.player_column) * cell_scale, sy + (0.5 + this.player_line) * cell_scale, cell_scale / 2 - 1); // -1 because otherwise it overfills + } + display_cell(line, column) { + sx = this.sx; + sy = this.sy; + cell_scale = this.cell_scale; + let shape = this.m[line][column]; + if (shape == WALL) { + if (cell_scale < 10) { + g.setColor(1, 0, 0).fillRect(sx + column * cell_scale, sy + line * cell_scale, sx + (column + 1) * cell_scale, sy + (line + 1) * cell_scale); + } else { + g.setColor(0.5, 0.5, 0.5).fillRect(sx + column * cell_scale, sy + line * cell_scale, sx + (column + 1) * cell_scale, sy + (line + 1) * cell_scale); + g.setColor(1, 0, 0).fillRect(sx + column * cell_scale, sy + (line + 0.15) * cell_scale, sx + (column + 0.35) * cell_scale, sy + (line + 0.45) * cell_scale); + g.fillRect(sx + (column + 0.55) * cell_scale, sy + (line + 0.15) * cell_scale, sx + (column + 1) * cell_scale, sy + (line + 0.45) * cell_scale); + g.fillRect(sx + column * cell_scale, sy + (line + 0.65) * cell_scale, sx + (column + 0.65) * cell_scale, sy + (line + 0.95) * cell_scale); + g.fillRect(sx + (column + 0.85) * cell_scale, sy + (line + 0.65) * cell_scale, sx + (column + 1) * cell_scale, sy + (line + 0.95) * cell_scale); + } + } else if (shape == BOX) { + let border = Math.floor((cell_scale - 2) / 4); + if (border > 0) { + g.setColor(0.6, 0.4, 0.3).fillRect(sx + column * cell_scale + 1, sy + line * cell_scale + 1, sx + (column + 1) * cell_scale - 1, sy + (line + 1) * cell_scale - 1); + g.setColor(0.7, 0.5, 0.5).fillRect(sx + column * cell_scale + 1 + border, sy + line * cell_scale + 1 + border, sx + (column + 1) * cell_scale - 1 - border, sy + (line + 1) * cell_scale - 1 - border); + } else { + g.setColor(0.7, 0.5, 0.5).fillRect(sx + column * cell_scale + 1, sy + line * cell_scale + 1, sx + (column + 1) * cell_scale - 1, sy + (line + 1) * cell_scale - 1); + } + } else if (shape == HOLE) { + g.setColor(1, 1, 1).fillRect(sx + column * cell_scale, sy + line * cell_scale, sx + (column + 1) * cell_scale - 1, sy + (line + 1) * cell_scale - 1); + g.setColor(0, 0, 1).drawRect(sx + column * cell_scale, sy + line * cell_scale, sx + (column + 1) * cell_scale - 1, sy + (line + 1) * cell_scale - 1); + } else if (shape == FILLED) { + let border = Math.floor((cell_scale - 2) / 4); + if (border > 0) { + g.setColor(0.6, 0.4, 0.3).fillRect(sx + column * cell_scale + 1, sy + line * cell_scale + 1, sx + (column + 1) * cell_scale - 1, sy + (line + 1) * cell_scale - 1); + g.setColor(0, 0, 1).fillRect(sx + column * cell_scale + 1 + border, sy + line * cell_scale + 1 + border, sx + (column + 1) * cell_scale - 1 - border, sy + (line + 1) * cell_scale - 1 - border); + } else { + g.setColor(0, 0, 1).fillRect(sx + column * cell_scale + 1 + border, sy + line * cell_scale + 1 + border, sx + (column + 1) * cell_scale - 1 - border, sy + (line + 1) * cell_scale - 1 - border); + + } + } else if (shape == SPACE) { + g.setColor(1, 1, 1).fillRect(sx + column * cell_scale, sy + line * cell_scale, sx + (column + 1) * cell_scale - 1, sy + (line + 1) * cell_scale - 1); + } + + } + display() { + g.clear(); + for (let line = 0; line < this.height; line++) { + for (let column = 0; column < this.width; column++) { + this.display_cell(line, column); + } + } + this.display_player(); + g.setColor(0, 0, 0).setFont("6x8:2") + .setFontAlign(0, -1, 0) + .drawString(map.name, g.getWidth() / 2, 0); + } +} + + +Bangle.on('touch', function(button, xy) { + if (in_menu) { + return; + } + let half_width = g.getWidth() / 2; + let half_height = g.getHeight() / 2; + let directions_amplitudes = [0, 0, 0, 0]; + directions_amplitudes[LEFT] = half_width - xy.x; + directions_amplitudes[RIGHT] = xy.x - half_width; + directions_amplitudes[UP] = half_height - xy.y; + directions_amplitudes[DOWN] = xy.y - half_height; + + let max_direction; + let second_max_direction; + if (directions_amplitudes[0] > directions_amplitudes[1]) { + max_direction = 0; + second_max_direction = 1; + } else { + max_direction = 1; + second_max_direction = 0; + } + for (let direction = 2; direction < 4; direction++) { + if (directions_amplitudes[direction] > directions_amplitudes[max_direction]) { + second_max_direction = max_direction; + max_direction = direction; + } else if (directions_amplitudes[direction] >= directions_amplitudes[second_max_direction]) { + second_max_direction = direction; + } + } + if (directions_amplitudes[max_direction] - directions_amplitudes[second_max_direction] > 10) { + // if there is little possible confusions between two candidate moves let's move. + // basically we forbid diagonals of 10 pixels wide + map.move(max_direction); + } + +}); + +Bangle.on('swipe', function(directionLR, directionUD) { + if (in_menu) { + return; + } + let last_move = history.pop(); + if (last_move !== undefined) { + map.undo(last_move[0], last_move[1]); + } +}); + +setWatch( + function() { + if (in_menu) { + return; + } + in_menu = true; + const menu = { + "": { + title: "choose action" + }, + "restart": function() { + E.showMenu(); + load_current_map(); + in_menu = false; + }, + "current map": { + value: config.current_maps[config.levels_set] + 1, + min: 1, + max: config.offsets[config.levels_set].length / 2, + onchange: (v) => { + config.current_maps[config.levels_set] = v - 1; + load_current_map(); + s.writeJSON("sokoban.json", config); + } + }, + "next map": function() { + E.showMenu(); + next_map(); + in_menu = false; + }, + "previous map": function() { + E.showMenu(); + previous_map(); + in_menu = false; + }, + "back to game": function() { + E.showMenu(); + g.clear(); + map.display(); + in_menu = false; + }, + }; + E.showMenu(menu); + }, + BTN1, { + repeat: true + } +); + + +Bangle.setLocked(false); + +current_map = config.current_map; +offsets = config.offsets; +load_current_map(); diff --git a/apps/sokoban/metadata.json b/apps/sokoban/metadata.json new file mode 100644 index 000000000..752c17e75 --- /dev/null +++ b/apps/sokoban/metadata.json @@ -0,0 +1,21 @@ +{ + "id": "sokoban", + "name": "Sokoban", + "shortName": "Sokoban", + "version": "0.02", + "description": "Classic Sokoban game (microban levels).", + "allow_emulator":false, + "icon": "sokoban.png", + "type": "app", + "tags": "game", + "screenshots": [{"url":"soko.png"}], + "supports": ["BANGLEJS", "BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"sokoban.app.js","url":"app.js"}, + {"name":"sokoban.microban.sok", "url":"sokoban.microban.sok"}, + {"name":"sokoban.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"sokoban.json"} + ] +} diff --git a/apps/sokoban/soko.png b/apps/sokoban/soko.png new file mode 100644 index 000000000..5bf0ae772 Binary files /dev/null and b/apps/sokoban/soko.png differ diff --git a/apps/sokoban/sokoban.microban.sok b/apps/sokoban/sokoban.microban.sok new file mode 100644 index 000000000..96146080c --- /dev/null +++ b/apps/sokoban/sokoban.microban.sok @@ -0,0 +1,1822 @@ +; 1 + +#### +# .# +# ### +#*@ # +# $ # +# ### +#### + +; 2 + +###### +# # +# #@ # +# $* # +# .* # +# # +###### + +; 3 + + #### +### #### +# $ # +# # #$ # +# . .#@ # +######### + +; 4 + +######## +# # +# .**$@# +# # +##### # + #### + +; 5 + + ####### + # # + # .$. # +## $@$ # +# .$. # +# # +######## + +; 6 + +###### ##### +# ### # +# $$ #@# +# $ #... # +# ######## +##### + +; 7 + +####### +# # +# .$. # +# $.$ # +# .$. # +# $.$ # +# @ # +####### + +; 8 + + ###### + # ..@# + # $$ # + ## ### + # # + # # +#### # +# ## +# # # +# # # +### # + ##### + +; 9 + +##### +#. ## +#@$$ # +## # + ## # + ##.# + ### + +; 10 + + ##### + #. # + #.# # +#######.# # +# @ $ $ $ # +# # # # ### +# # +######### + +; 11 + + ###### + # # + # ##@## +### # $ # +# ..# $ # +# # +# ###### +#### + +; 12 + +##### +# ## +# $ # +## $ #### + ###@. # + # .# # + # # + ####### + +; 13 + +#### +#. ## +#.@ # +#. $# +##$ ### + # $ # + # # + # ### + #### + +; 14 + +####### +# # +# # # # +#. $*@# +# ### +##### + +; 15 + + ### +######@## +# .* # +# # # +#####$# # + # # + ##### + +; 16 + + #### + # #### + # ## +## ## # +#. .# @$## +# # $$ # +# .# # +########## + +; 17 + +##### +# @ # +#...# +#$$$## +# # +# # +###### + +; 18 + +####### +# # +#. . # +# ## ## +# $ # +###$ # + #@ # + # # + #### + +; 19 + +######## +# .. # +# @$$ # +##### ## + # # + # # + # # + #### + +; 20 + +####### +# ### +# @$$..# +#### ## # + # # + # #### + # # + #### + +; 21 + +#### +# #### +# . . # +# $$#@# +## # + ###### + +; 22 + +##### +# ### +#. . # +# # # +## # # + #@$$ # + # # + # ### + #### + +; 23 + +####### +# * # +# # +## # ## + #$@.# + # # + ##### + +; 24 + +# ##### + # # +###$$@# +# ### +# # +# . . # +####### + +; 25 + + #### + # ### + # $$ # +##... # +# @$ # +# ### +##### + +; 26 + + ##### + # @ # + # # +###$ # +# ...# +# $$ # +### # + #### + +; 27 + +###### +# .# +# ## ## +# $$@# +# # # +#. ### +##### + +; 28 + +##### +# # +# @ # +# $$### +##. . # + # # + ###### + +; 29 + + ##### + # ## + # # + ###### # +## #. # +# $ $ @ ## +# ######.# +# # +########## + +; 30 + +#### +# ### +# $$ # +#... # +# @$ # +# ## +##### + +; 31 + + #### + ## # +##@$.## +# $$ # +# . . # +### # + ##### + +; 32 + + #### +## ### +# # +#.**$@# +# ### +## # + #### + +; 33 + +####### +#. # # +# $ # +#. $#@# +# $ # +#. # # +####### + +; 34 + + #### +### #### +# # +#@$***. # +# # +######### + +; 35 + + #### + ## # + #. $# + #.$ # + #.$ # + #.$ # + #. $## + # @# + ## # + ##### + +; 36 + +#### +# ############ +# $ $ $ $ $ @ # +# ..... # +############### + +; 37 + + ### +##### #.# +# ###.# +# $ #.# +# $ $ # +#####@# # + # # + ##### + +; 38 + +########## +# # +# ##.### # +# # $$ . # +# . @$## # +##### # + ###### + +; 39 + +##### +# #### +# # # .# +# $ ### +### #$. # +# #@ # +# # ###### +# # +##### + +; 40 + + ##### + # # +## ## +# $$$ # +# .+. # +####### + +; 41 + +####### +# # +#@$$$ ## +# #...# +## ## + ###### + +; 42 + + #### + # # + #@ # +####$.# +# $.# +# # $.# +# ## +###### + +; 43 + + #### + # @# + # # +###### .# +# $ .# +# $$# .# +# #### +### # + #### + +; 44 'Duh!' + +##### +#@$.# +##### + +; 45 + +###### +#... # +# $ # +# #$## +# $ # +# @ # +###### + +; 46 + + ###### +## # +# ## # +# # $ # +# * .# +## #@## + # # + ##### + +; 47 + + ####### +### # +# $ $ # +# ### ##### +# @ . . # +# ### # +##### ##### + +; 48 + +###### +# @ # +# # ## +# .# ## +# .$$$ # +# .# # +#### # + ##### + +; 49 + +###### +# @ # +# $# # +# $ # +# $ ## +### #### + # # # + #... # + # # + ####### + +; 50 + + #### +### ##### +# $ @..# +# $ # # +### #### # + # # + ######## + +; 51 + +#### +# ### +# ### +# $*@ # +### .# # + # # + ###### + +; 52 + + #### +### @# +# $ # +# *.# +# *.# +# $ # +### # + #### + +; 53 + + ##### +##. .## +# * * # +# # # +# $ $ # +## @ ## + ##### + +; 54 + + ###### + # # + ##### . # +### ###. # +# $ $ . ## +# @$$ # . # +## ##### + ###### + +; 55 + +######## +# @ # # +# # +#####$ # + # ### + ## #$ ..# + ## # ### + #### + +; 56 + +##### +# ### +# $ # +##* . # + # @# + ###### + +; 57 + + #### + # # + #@ # + # # +### #### +# * # +# $ # +#####. # + #### + +; 58 + +#### +# #### +#.*$ # +# .$# # +## @ # + # ## + ##### + +; 59 + +############ +# # +# ####### @## +# # # +# # $ # # +# $$ ##### # +### # # ...# + #### # # + ###### + +; 60 + + ######### + # # +##@##### # +# # # # +# # $.# +# ##$##.# +##$## #.# +# $ #.# +# # ### +######## + +; 61 + +######## +# # +# #### # +# #...@# +# ###$### +# # # +# $$ $ # +#### ## + #.### + ### + +; 62 + + ########## +#### ## # +# $$$....$@# +# ### # +# #### #### +##### + +; 63 + +##### #### +# ##### .# +# $ ######## +### #### .$ @ # + # # # #### # + #### #### ##### + +; 64 + + ###### +## # +# $ # +# $$ # +### .##### + ##.# @ # + #. $ # + #. #### + #### + +; 65 + + ###### + # # + # $ # + ####$ # +## $ $ # +#....# ## +# @ # +## # # + ######## + +; 66 + + ### + #@# + ###$### +## . ## +# # # # +# # # # +# # # # +# # # # +# # # # +## $ $ ## + ##. .## + # # + # # + ##### + +; 67 + +##### +# ## +# # # +#@$*.## +## . # + # $# # + ## # + ##### + +; 68 + + #### + # ###### +## $ # +# .# $ # +# .#$##### +# .@ # +###### + +; 69 + +#### #### +# #### # +# # # # +# # $## +# . .#$ # +#@ ## # $ # +# . # # +########### + +; 70 + +##### +# @ #### +# # +# $ $$ # +##$## # +# #### +# .. # +##.. # + ### # + #### + +; 71 + +########### +# # ### +# $@$ # . .# +# ## ### ## # +# # # # +# # # # # +# ######### # +# # +############# + +; 72 + + #### + ## ##### + # $ @ # + # $# # +#### ##### +# # # +# $ # +# ..# # +# .#### +# ## +#### + +; 73 + +#### +# ##### +# $$ $ # +# # +## ## ## +#...#@# +# ### ## +# # +# # # +######## + +; 74 + + #### + # ####### + #$ @# .# +## #$$ .# +# $ ##..# +# # ##### +### # + ##### + +; 75 + + ####### +## ....## +# ###### +# $ $ @# +### $ $ # + ### # + ###### + +; 76 + + ##### +## # +# ##### +# #.# # +#@ #.# $ # +# #.# ## +# # # +## ##$$# + ## # + # #### + #### + +; 77 + +########## +# @ .... # +# ####$## +## # $ $ # + # $ # + # ###### + ##### + +; 78 + + ####### +## ## +# $ $ # +# $ $ $ # +## ### #### + #@ .....# + ## ### + ####### + +; 79 + + ######### + # # # +## $#$# # +# .$.@ # +# .# # +########## + +; 80 + +#### +# ####### +# . ## .# +# $# .# +## ## # .# + # # # + #### # # + # @$ ### + # $$ # + # # + ###### + +; 81 + + ##### + # # + # . # +## * # +# *## +# @## +## $ # + # # + ##### + +; 82 + +##### +# ### +# . ## +##*#$ # +# .# $ # +# @## ## +# # +####### + +; 83 + +###### +# ## +# $ $ ## +## $$ # + # # # + # ## ## + # . .# + # @. .# + # #### + #### + +; 84 + +######## +# ... # +# ### ## +# # $ # +## #@$ # + # # $ # + # ### ##### + # # + # ### # + ##### ##### + +; 85 + + #### + ####### # + # $ # + # $ $ # + # ######## +## # . # +# # # # +# @ . ## +## # # # + # . # + ####### + +; 86 + + #### + ### ## + ## $ # +## $ # # +# @#$$ # +# .. ### +# ..### +##### + +; 87 + + #### +###### # +# # +# ... .# +##$###### +# $ # +# $### +## $ # + ## @ # + ###### + +; 88 + + #### + # ### # + # # # + # # # # + # #$ #.# + # # # # # + # #$ #.# # + # # # # +####$ #.# # +# @ # # +# # ## # +######## + +; 89 + +########## +# ## # +# $ $@# # +#### # $ # + #.# ## + # #.# $# + # #. # + # #. # + ###### + +; 90 + + ######## + # @ # + # $ $ # +### ## ### +# $..$ # +# .. # +########## + +; 91 + +########### +# .## # +# $$@..$$ # +# ##. # +########### + +; 92 + + #### + # # ##### + # # # # + # ######.# # +#### $ . # +# $$# ###.# # +# # # # # +######### #@ ## + # # + #### + +; 93 + + ######### +## # ## +# # # +# $ # $ # +# *.* # +####.@.#### +# *.* # +# $ # $ # +# # # +## # ## + ######### + +; 94 + +######### +# @ # # +# $ $ # +##$### ## +# ... # +# # # +###### # + #### + +; 95 + +######## +#@ # +# .$$. # +# $..$ # +# $..$ # +# .$$. # +# # +######## + +; 96 + + ###### + # # + # # +##### # +# #.##### +# $@$ # +#####.# # + ## ## ## + # $.# + # ### + ##### + +; 97 + + #### + # ######## +#### $ $.....# +# $ ###### +#@### ### +# $ # +# $ # # +## # # + # # + ###### + +; 98 + +##### +# ## #### +# $ ### .# +# $ $ .# +## $#####.# #### +# $ # # .### # +# # # .# @ # +### # # # + #### ## ## + ####### + +; 99 + + ##### + # # +####### ####### # # +# # # # # +# @ #### # #### +# # ....## #### # +# ##### ## $$ $ $ # +###### # # + # ########## + #### + +; 100 + +####### +# @# # +#.$ # +#. # $## +#.$# # +#. # $ # +# # # +######## + +; 101 'Lockdown' + + ##### + # # + # # ####### + # * # # + ## ## # # + # #* # +### # # # ### +# *#$+ # +# # ## ## +# # * # +####### # # + # # + ##### + +; 102 + +########### +#....# # +# # $$ # +# @ ## # +# ##$ # +###### $ # + # # + ###### + +; 103 + + ##### + # . ## +### $ # +# . $#@# +# #$ . # +# $ ### +## . # + ##### + +; 104 + + ##### +##### # +# $ # +# $#$#@# +### # # + # ... # + ### ## + # # + #### + +; 105 + + #### #### +## ### ## +# # # # +# *. .* # +###$ $### + # @ # +###$ $### +# *. .* # +# # # # +## ### ## + #### #### + +; 106 + + ######## + # # + #@ $ # +## ###$ # +# .....### +# $ $ $ # +###### # # + # # + ##### + +; 107 + +######## +# # +# $*** # +# * * # +# * * # +# ***. # +# @# +######## + +; 108 + +#### ##### +# ### # ## +# # #$ $ # +#..# ##### # # +# @ # $ $ # +#..# ## +## ######### + ##### + +; 109 + + ####### +# # # +# # # # # + # @ $ # +### ### # +# ### # +# $ ##.# +## $ #.# + ## $ .# +# ## $#.# +## ## #.# +### # # +### ##### + +; 110 + + #### + # # + # $#### +###. . # +# $ # $ # +# . .### +####$ # + # @# + #### + +; 111 + +###### +# #### +# ...# +# ...# +###### # + # # # + # $$ ## + # @$ # + # $$ # + ## $# # + # # + ###### + +; 112 + + ##### +## #### +# $$$ # +# # $ # +# $## ## +### #. # + # # # + ##### ### + # # ## + # @....# + # # + # # # + ######## + +; 113 + + ##### + ## # +### # # +# . # +# ## ##### +# . . # ## +# # @ $ ### +#####. # $ # + #### $ # + ## $ ## + # ## + # # + #### + +; 114 + +###### +# ### +# # $ # +# $ @ # +## ## ##### +# #......# +# $ $ $ $ # +## ###### + ##### + +; 115 + + ##### +##### #### +# # # +# #..... # +## ## # ### + #$$@$$$ # + # ### + ####### + +; 116 + + ##### + ### # +####.....# +# @$$$$$ # +# # ## +##### # + ##### + +; 117 + + #### #### + # ### ## + # @ # +##..### # +# # # +#...#$ # # +# ## $$ $ # +# $ ### +#### ### + #### + +; 118 + + ##### +## ## +# $ ## +# $ $ ## +###$# . ## + # # . # + ## ##. # + # @ . ## + # # # + ######## + +; 119 + + ###### + # ## + ## ## # + # $$ # # + # @$ # # + # # # +#### # # +# ... ## +# ## +####### + +; 120 + + #### +####### # +# $ ## +# $##### # +# @# # # +## ##.. # +# # ..#### +# $ ### +# $### +# # +#### + +; 121 + + ###### + # . # +##$.# # +# * # +# ..### +##$ # ##### +## ## # # +# #### # # +# @ $ $ # +## # # + ########## + +; 122 + +##### +# ### +# #$ # +# $ # +# $ $ # +# $# # +# @### +## ######## +# ...# +# # +########..# + #### + +; 123 + +######## +# # +# $ $$ ######## +##### @##. . # + #$ # . # + # #. . ## + #$# ## # # + # # + # ### ## + # # #### + #### + +; 124 + +############## +# # # +# $@$$ # . ..# +## ## ### ## # + # # # # + # # # # # + # ######### # + # # + ############# + +; 125 + + ##### + # ## + # $ # +######## #@## +# . # $ $ # +# $# # +#...##### # +##### ##### + +; 126 + + ########### +##....... # +# $$$$$$$@ # +# # # # ## +# # # # +# ####### +##### + +; 127 + +## #### +#### #### + # $ $. # +## # .$ # +# ##.### +# $ . # +# @ # # +# ###### +#### + +; 128 + + ######### +### # # +# * $ . . # +# $ ## ## +####*# # + # @ ### + # ### + ##### + +; 129 + + ######### +### @ # # +# * $ *.. # +# $ # # +####*# ### + # ## + # ### + ##### + +; 130 + +##### ##### +# ####.. # +# $$$ # +# $# .. # +### @# ## # + # ## # + ########## + +; 131 + +##### +# # +# . # +#.@.### +##.# # +# $ # +# $ # +##$$ # + # ### + # # + #### + +; 132 + +#### +# @### +#.* ##### +#..#$$ $ # +## # + # # ## # + # ##### + ##### + +; 133 + + ####### + # . .### + # . . . # +### #### # +# @$ $ # +# $$ $ # +#### ### + ##### + +; 134 + + #### +######### # +# ## $ # +# $ ## # +### #. .# ## + # #. .#$## + # # # # + # @ $ # + # ####### + #### + +; 135 + +####### +# ##### +# $$#@##..# +# # # +# $ # # # +#### $ ..# + ######## + +; 136 + + ####### + # # +## ###$## +#.$ @ # +# .. #$ # +#.## $ # +# #### +###### + +; 137 + + #### + ## ### +#### # $ # +# #### $ $ # +# ..# #$ # +# # @ ### +## #..# ### + # ## # # + # # + ######## + +; 138 + + #### +### # +# ### +# # . .# +# @ ...#### +# # # # ## +# # $$ # +##### $ $ # + ##$ # ## + # # + ###### + +; 139 + + #### +## #### +# ...# +# ...# +# # ## +# #@ #### #### +##### $ ### # + # ##$ $ # + ### $$ # + # $ ## ### + # ###### + ###### + +; 140 + +######## ##### +# # ### # +# ## $ # +#.# @ ## $ ## +#.# # $ ## +#.# $ ## +#. ## ##### +## # + ###### + +; 141 + + ######## + # # . # + # .*.# + # # * # +####$##.## +# $ # +# $ ## $ # +# @# # +########## + +; 142 + + #### + # # + # #### +###$.$ # +# .@. # +# $.$### +#### # + # # + #### + +; 143 + +#### +# #### +# $ # +# .# # +# $# ## +# . # +#### # + # # + ### ### + # $ # +## #$# ## +# $ @ $ # +# ..#.. # +### ### + ##### + +; 144 + + #### + ### ##### + # $$ # # + # $ . .$$## + # .. #. $ # +### #** . # +# . **# ### +# $ .# .. # +##$$.@. $ # + # # $$ # + ##### ### + #### + +; 145 + + ##### + # @ # + ## ## +###.$$$.### +# $...$ # +# $.#.$ # +# $...$ # +###.$$$.### + ## ## + # # + ##### + +; 146 + + ####### +## . ## +# .$$$. # +# $. .$ # +#.$ @ $.# +# $. .$ # +# .$$$. # +## . ## + ####### + +; 147 + + ##### +######## # +#. . @#.# +# ### # +## $ # # + # $ ##### + # $# # + ## # # + # ## + ##### + +; 148 'from (Original 18)' + +########### +# . # # +# #. @ # +# #..# ####### +## ## $$ $ $ # + ## # + ############# + +; 149 'from (Boxxle 43)' + + #### +## ### +#@$ # +### $ # + # ###### + # $....# + # # #### + ## # # + # $# # + # # + # ### + #### + +; 150 'from (Original 47)' + + #### + ##### # + # $####### +## ## ..# ...# +# $ $$#$ @ # +# ### # +####### # #### + #### + +; 151 'from (Original 47)' + + #### + # # + ### # +## $ # +# # # +# #$$ ###### +# # # .# +# $ @ .# +### ####..# + #### #### + +; 152 + +###### #### +# # # +#.## #$## # +# # # # +#$ # ### # # +# # # # # +# # #### # # # +#. @ $ * . # +############### + +; 153 + +############# +#.# @# # # +#.#$$ # $ # +#.# # $# # +#.# $# # $## +#.# # $# # +#.# $# # $# +#.. # $ # +#.. # # # +############ + +; 154 'Take the long way home.' + + ############################ + # # + # ######################## # + # # # # + # # #################### # # + # # # # # # + # # # ################ # # # + # # # # # # # # + # # # # ############ # # # # + # # # # # # # # # + # # # # # ############ # # # + # # # # # # # # + # # # # ################ # # + # # # # # # +##$# # #################### # +#. @ # # +############################# + +; 155 'The Dungeon' + + ###### #### +#####*# ################# ## +# ### # +# ######## #### ## # +### #### # #### #### ## +#*# # .# # # # # # # +#*# # # # ## # ## ## # +### ### ### # ## # ## ## + # # #*# # # # # + # # ### ##### #### # # + ##### ##### ####### ###### + # # # #**# # +## # # #**# ####### ## # +# ######### # ##### ### +# # # $ #*# +# ######### ### @##### #*# +##### #### #### ###### diff --git a/apps/sokoban/sokoban.png b/apps/sokoban/sokoban.png new file mode 100644 index 000000000..849b92d01 Binary files /dev/null and b/apps/sokoban/sokoban.png differ diff --git a/apps/spacew/README.md b/apps/spacew/README.md new file mode 100644 index 000000000..4f2ca3f00 --- /dev/null +++ b/apps/spacew/README.md @@ -0,0 +1,43 @@ +# Space Weaver ![](app.png) + +Vector map + +Written by: [Pavel Machek](https://github.com/pavelmachek) + +Space Weaver is application for displaying vector maps. It is +currently suitable for developers, and more work is needed. + +Maps can be created from openstreetmap extracts. Those are cut using +osmosis, then translated into geojson. Geojson is further processes to +add metadata such as colors, and to split it into xyz tiles, while +keeping geojson format. Tiles are then merged into single file, which +can be uploaded to the filesystem. Index at the end provides locations +of the tiles. + +## Preparing data + +Tools in spacew/prep can be used to prepare data. + +You'll need to edit prepare.sh to point it to suitable osm extract, +and you'll need to select area of interest. Start experiments with +small area. You may want to delete cstocs and provide custom +conversion to ascii. + +Details of which features are visible at what zoom levels can be +configured in split.js. This can greatly affect file sizes. Then +there's "meta.max_zoom = 17" setting, reduce it if file is too big. + +For initial experiments, configure things so that mtar file is around +500KB. (I had troubles with big files, both on hardware and to lesser +extent on simulator. In particular, mtar seemed to be corrupted after +emulator window was closed.) + +## Future Development + +Directories at the end of .mtar should be hashed, not linear searched. + +Geojson is not really suitable as it takes a lot of storage. + +It would be nice to support polygons. + +Web-based tool for preparing maps would be nice. \ No newline at end of file diff --git a/apps/spacew/app-icon.js b/apps/spacew/app-icon.js new file mode 100644 index 000000000..27b6e2662 --- /dev/null +++ b/apps/spacew/app-icon.js @@ -0,0 +1,2 @@ +require("heatshrink").decompress(atob("mEwwkE/8Ql//j//AAUD//yAgILBAAXzBQMxAoMwn4XKBIgXCmAXEh4XF+IJGC4XxAoMgl/zgX/nASBBgPwIoIEBmYBBI4ug1/6hX/zOf+UBEIMP+UC+eZAIPyhP/yAnB0Ef+QXBnM/GgUwh4ECwX/wYvCkIvB+BrBA4JsFAQMiL4gRBA4XxNYIlBBgQGBiJXEBQRnBiYoEiQXFgURT4YAB+QXBS4RTCJoQMBj4gBWQPwN4IKCNgLHDRAIlDEgIxBC4zHBJITACC4gMB+MfAIJCCRIU/GIIGCEoLyCBgQOCgZAEBAL5CC4UvC4oFBMIJ9CCAQMBPwbABKoYMBJ4KpBZQgKBVwnyh/wKoQMBVoUgn4XFmTGEgfxC4QKBCQRKBeAYtBkYXFXYIFBkTfCSgMfIIYbBdwTADJIIEBkYEDAYKyDC4J9DKoSFDiZMDGYKCDkbWEKoUzIQQREHQIFDifzBQYXGIIIMDkDwDN4IXFIIIXBJQMhEQqCCT4IWENoUCC4MvXwTjCiZqBEQIXGNoITBC4LRDEQMDHQbWEAAUDIYPzmabEEQIXDmYXGiUgFAMyLASQDXgPzj7uEQobNB+MxWYsgHQKSBEQqFCUYPwUwgKCHQUvEQqFCkAXCBQ0Qn/xmYXH+IXB+S+ESAUAEQMzHQqFCgEvmS+EBQUBl/wUw4MDmS+ESAcf+ExC44MCmS+ESAcPmAvI/8hh8iNY8wgcwaw4MCh8hNY/wC5kDTwKbHgThGEgsQQZMhdw61CgSmGAAUANRAkCgUTBZEQiRSHHga+HNYUCC5I8BXw4XCgIWJHgJTJ+IXJHAIXB+eTJoIJD+fyC4LABYQWZBQOYC4Mf+eS/85DgIJBxMygAFB+YUBC4YqBkAoBAIM5n4JCAgIvBwYBCNgyDKTRIXM+YXFA=")) + diff --git a/apps/spacew/app.js b/apps/spacew/app.js new file mode 100644 index 000000000..e438799f6 --- /dev/null +++ b/apps/spacew/app.js @@ -0,0 +1,620 @@ +/* original openstmap.js */ + +/* OpenStreetMap plotting module. + +Usage: + +var m = require("openstmap"); +// m.lat/lon are now the center of the loaded map +m.draw(); // draw centered on the middle of the loaded map + +// plot gps position on map +Bangle.on('GPS',function(f) { + if (!f.fix) return; + var p = m.latLonToXY(fix.lat, fix.lon); + g.fillRect(p.x-2, p.y-2, p.x+2, p.y+2); +}); + +// recenter and redraw map! +function center() { + m.lat = fix.lat; + m.lon = fix.lon; + m.draw(); +} + +// you can even change the scale - eg 'm/scale *= 2' + +*/ + +var exports = {}; +var m = exports; +m.maps = require("Storage").list(/openstmap\.\d+\.json/).map(f=>{ + let map = require("Storage").readJSON(f); + map.center = Bangle.project({lat:map.lat,lon:map.lon}); + return map; +}); +// we base our start position on the middle of the first map +if (m.maps[0] != undefined) { + m.map = m.maps[0]; + m.scale = m.map.scale; // current scale (based on first map) + m.lat = m.map.lat; // position of middle of screen + m.lon = m.map.lon; // position of middle of screen +} else { + m.scale = 20; + m.lat = 50; + m.lon = 14; +} + +exports.draw = function() { + var cx = g.getWidth()/2; + var cy = g.getHeight()/2; + var p = Bangle.project({lat:m.lat,lon:m.lon}); + m.maps.forEach((map,idx) => { + var d = map.scale/m.scale; + var ix = (p.x-map.center.x)/m.scale + (map.imgx*d/2) - cx; + var iy = (map.center.y-p.y)/m.scale + (map.imgy*d/2) - cy; + var o = {}; + var s = map.tilesize; + if (d!=1) { // if the two are different, add scaling + s *= d; + o.scale = d; + } + //console.log(ix,iy); + var tx = 0|(ix/s); + var ty = 0|(iy/s); + var ox = (tx*s)-ix; + var oy = (ty*s)-iy; + var img = require("Storage").read(map.fn); + // fix out of range so we don't have to iterate over them + if (tx<0) { + ox+=s*-tx; + tx=0; + } + if (ty<0) { + oy+=s*-ty; + ty=0; + } + var mx = g.getWidth(); + var my = g.getHeight(); + for (var x=ox,ttx=tx; x ac * expansion) && (x = ac * expansion); + (y > ac) && (y = ac); + //(x < 0) && (x = 0); + //(y < 0) && (y = 0); + return [x, y]; +} + +// Convert bbox to xyx bounds +// +// - `bbox` {Number} bbox in the form `[w, s, e, n]`. +// - `zoom` {Number} zoom. +// - `tms_style` {Boolean} whether to compute using tms-style. +// - `srs` {String} projection of input bbox (WGS84|900913). +// - `@return` {Object} XYZ bounds containing minX, maxX, minY, maxY properties. +xyz = function(bbox, zoom, tms_style, srs) { + // If web mercator provided reproject to WGS84. + if (srs === '900913') { + bbox = this.convert(bbox, 'WGS84'); + } + + var ll = [bbox[0], bbox[1]]; // lower left + var ur = [bbox[2], bbox[3]]; // upper right + var px_ll = px(ll, zoom); + var px_ur = px(ur, zoom); + // Y = 0 for XYZ is the top hence minY uses px_ur[1]. + var size = 256; + var x = [ Math.floor(px_ll[0] / size), Math.floor((px_ur[0] - 1) / size) ]; + var y = [ Math.floor(px_ur[1] / size), Math.floor((px_ll[1] - 1) / size) ]; + var bounds = { + minX: Math.min.apply(Math, x) < 0 ? 0 : Math.min.apply(Math, x), + minY: Math.min.apply(Math, y) < 0 ? 0 : Math.min.apply(Math, y), + maxX: Math.max.apply(Math, x), + maxY: Math.max.apply(Math, y) + }; + if (tms_style) { + var tms = { + minY: (Math.pow(2, zoom) - 1) - bounds.maxY, + maxY: (Math.pow(2, zoom) - 1) - bounds.minY + }; + bounds.minY = tms.minY; + bounds.maxY = tms.maxY; + } + return bounds; +}; + +// Convert screen pixel value to lon lat +// +// - `px` {Array} `[x, y]` array of geographic coordinates. +// - `zoom` {Number} zoom level. +function ll(px, zoom) { + var size = 256 * Math.pow(2, zoom); + var bc = (size / 360); + var cc = (size / (2 * Math.PI)); + var zc = size / 2; + var g = (px[1] - zc) / -cc; + var lon = (px[0] - zc) / bc; + var R2D = 180 / Math.PI; + var lat = R2D * (2 * Math.atan(Math.exp(g)) - 0.5 * Math.PI); + return [lon, lat]; +} + +// Convert tile xyz value to bbox of the form `[w, s, e, n]` +// +// - `x` {Number} x (longitude) number. +// - `y` {Number} y (latitude) number. +// - `zoom` {Number} zoom. +// - `tms_style` {Boolean} whether to compute using tms-style. +// - `srs` {String} projection for resulting bbox (WGS84|900913). +// - `return` {Array} bbox array of values in form `[w, s, e, n]`. +bbox = function(x, y, zoom, tms_style, srs) { + var size = 256; + + // Convert xyz into bbox with srs WGS84 + if (tms_style) { + y = (Math.pow(2, zoom) - 1) - y; + } + // Use +y to make sure it's a number to avoid inadvertent concatenation. + var ll_ = [x * size, (+y + 1) * size]; // lower left + // Use +x to make sure it's a number to avoid inadvertent concatenation. + var ur = [(+x + 1) * size, y * size]; // upper right + var bbox = ll(ll_, zoom).concat(ll(ur, zoom)); + + // If web mercator requested reproject to 900913. + if (srs === '900913') { + return this.convert(bbox, '900913'); + } else { + return bbox; + } +}; + +/* original openstmap_app.js */ + +//var m = require("openstmap"); +var HASWIDGETS = true; +var R; +var fix = {}; +var mapVisible = false; +var hasScrolled = false; +var settings = require("Storage").readJSON("openstmap.json",1)||{}; +var points; +var startDrag = 0; + +// Redraw the whole page +function redraw(qual) { + if (1) drawAll(qual); + g.setClipRect(R.x,R.y,R.x2,R.y2); + if (0) m.draw(); + drawPOI(); + drawMarker(); + // if track drawing is enabled... + if (settings.drawTrack) { + if (HASWIDGETS && WIDGETS["gpsrec"] && WIDGETS["gpsrec"].plotTrack) { + g.setColor("#f00").flip(); // force immediate draw on double-buffered screens - track will update later + WIDGETS["gpsrec"].plotTrack(m); + } + if (HASWIDGETS && WIDGETS["recorder"] && WIDGETS["recorder"].plotTrack) { + g.setColor("#f00").flip(); // force immediate draw on double-buffered screens - track will update later + WIDGETS["recorder"].plotTrack(m); + } + } + g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); +} + +// Draw the POIs +function drawPOI() { + if (1) return; + /* var waypoints = require("waypoints").load(); FIXME */ g.setFont("Vector", 18); + waypoints.forEach((wp, idx) => { + var p = m.latLonToXY(wp.lat, wp.lon); + var sz = 2; + g.setColor(0,0,0); + g.fillRect(p.x-sz, p.y-sz, p.x+sz, p.y+sz); + g.setColor(0,0,0); + g.drawString(wp.name, p.x, p.y); + print(wp.name); + }) +} + + + +// Draw the marker for where we are +function drawMarker() { + if (!fix.fix) return; + var p = m.latLonToXY(fix.lat, fix.lon); + g.setColor(1,0,0); + g.fillRect(p.x-2, p.y-2, p.x+2, p.y+2); +} + +Bangle.on('GPS',function(f) { + fix=f; + if (HASWIDGETS) WIDGETS["sats"].draw(WIDGETS["sats"]); + if (mapVisible) drawMarker(); +}); +Bangle.setGPSPower(1, "app"); + +if (HASWIDGETS) { + Bangle.loadWidgets(); + WIDGETS["sats"] = { area:"tl", width:48, draw:w=>{ + var txt = (0|fix.satellites)+" Sats"; + if (!fix.fix) txt += "\nNO FIX"; + g.reset().setFont("6x8").setFontAlign(0,0) + .drawString(txt,w.x+24,w.y+12); + } + }; + Bangle.drawWidgets(); +} +R = Bangle.appRect; + +function showMap() { + mapVisible = true; + g.reset().clearRect(R); + redraw(0); + emptyMap(); +} + +function emptyMap() { + Bangle.setUI({mode:"custom",drag:e=>{ + if (e.b) { + if (!startDrag) + startDrag = getTime(); + g.setClipRect(R.x,R.y,R.x2,R.y2); + g.scroll(e.dx,e.dy); + m.scroll(e.dx,e.dy); + g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); + hasScrolled = true; + print("Has scrolled"); + } else if (hasScrolled) { + delta = getTime() - startDrag; + startDrag = 0; + hasScrolled = false; + print("Done", delta, e.x, e.y); + qual = 0; + if (delta < 0.2) { + if (e.x < g.getWidth() / 2) { + if (e.y < g.getHeight() / 2) { + m.scale /= 2; + } else { + m.scale *= 2; + } + } else { + if (e.y < g.getHeight() / 2) { + qual = 2; + } else { + qual = 4; + } + } + } + g.reset().clearRect(R); + redraw(qual); + } + }, btn: btn=>{ + mapVisible = false; + var menu = {"":{title:"Map"}, + "< Back": ()=> showMap(), + /*LANG*/"Zoom In": () =>{ + m.scale /= 2; + showMap(); + }, + /*LANG*/"Zoom Out": () =>{ + m.scale *= 2; + showMap(); + }, + /*LANG*/"Draw Track": { + value : !!settings.drawTrack, + onchange : v => { settings.drawTrack=v; require("Storage").writeJSON("openstmap.json",settings); } + }, + /*LANG*/"Center Map": () =>{ + m.lat = m.map.lat; + m.lon = m.map.lon; + m.scale = m.map.scale; + showMap(); + }, + /*LANG*/"Benchmark": () =>{ + m.lat = 50.001; + m.lon = 14.759; + m.scale = 2; + g.reset().clearRect(R); + redraw(18); + print("Benchmark done (31 sec)"); + } + }; + if (fix.fix) menu[/*LANG*/"Center GPS"]=() =>{ + m.lat = fix.lat; + m.lon = fix.lon; + showMap(); + }; + E.showMenu(menu); + }}); +} + +var gjson = null; + +function readTarFile(tar, f) { + const st = require('Storage'); + json_off = st.read(tar, 0, 16) * 1; + if (isNaN(json_off)) { + print("Don't have archive", tar); + return undefined; + } + while (1) { + json_len = st.read(tar, json_off, 6) * 1; + if (json_len == -1) + break; + json_off += 6; + json = st.read(tar, json_off, json_len); + //print("Have directory, ", json.length, "bytes"); + //print(json); + files = JSON.parse(json); + //print(files); + rec = files[f]; + if (rec) + return st.read(tar, rec.st, rec.si); + json_off += json_len; + } + return undefined; +} + +function loadVector(name) { + var t1 = getTime(); + print(".. Read", name); + //s = require("Storage").read(name); + var s = readTarFile("delme.mtar", name); + if (s == undefined) { + print("Don't have file", name); + return null; + } + var r = JSON.parse(s); + print(".... Read and parse took ", getTime()-t1); + return r; +} + +function drawPoint(a) { + lon = a.geometry.coordinates[0]; + lat = a.geometry.coordinates[1]; + + var p = m.latLonToXY(lat, lon); + var sz = 2; + if (a.properties["marker-color"]) { + g.setColor(a.properties["marker-color"]); + } + if (a.properties.marker_size == "small") + sz = 1; + if (a.properties.marker_size == "large") + sz = 4; + + g.fillRect(p.x-sz, p.y-sz, p.x+sz, p.y+sz); + g.setColor(0,0,0); + g.setFont("Vector", 18).setFontAlign(-1,-1); + g.drawString(a.properties.name, p.x, p.y); + points ++; +} + +function drawLine(a, qual) { + lon = a.geometry.coordinates[0][0]; + lat = a.geometry.coordinates[0][1]; + i = 1; + step = 1; + len = a.geometry.coordinates.length; + step = step * qual; + var p1 = m.latLonToXY(lat, lon); + if (a.properties.stroke) { + g.setColor(a.properties.stroke); + } + while (i < len) { + lon = a.geometry.coordinates[i][0]; + lat = a.geometry.coordinates[i][1]; + var p2 = m.latLonToXY(lat, lon); + + //print(p1.x, p1.y, p2.x, p2.y); + g.drawLine(p1.x, p1.y, p2.x, p2.y); + if (i == len-1) + break; + i = i + step; + if (i>len) + i = len-1; + points ++; + p1 = p2; + g.flip(); + } +} + +function drawVector(gjson, qual) { + var d = gjson; + points = 0; + var t1 = getTime(); + + for (var a of d.features) { + if (a.type != "Feature") + print("Expecting feature"); + g.setColor(0,0,0); + // marker-size, marker-color, stroke + if (qual < 32 && a.geometry.type == "Point") + drawPoint(a); + if (qual < 8 && a.geometry.type == "LineString") + drawLine(a, qual); + } + print("....", points, "painted in", getTime()-t1, "sec"); +} + +function fname(lon, lat, zoom) { + var bbox = [lon, lat, lon, lat]; + var r = xyz(bbox, 13, false, "WGS84"); + //console.log('fname', r); + return 'z'+zoom+'-'+r.minX+'-'+r.minY+'.json'; +} + +function fnames(zoom) { + var bb = [m.lon, m.lat, m.lon, m.lat]; + var r = xyz(bb, zoom, false, "WGS84"); + while (1) { + var bb2 = bbox(r.minX, r.minY, zoom, false, "WGS84"); + var os = m.latLonToXY(bb2[3], bb2[0]); + if (os.x >= 0) + r.minX -= 1; + else if (os.y >= 0) + r.minY -= 1; + else break; + } + while (1) { + var bb2 = bbox(r.maxX, r.maxY, zoom, false, "WGS84"); + var os = m.latLonToXY(bb2[1], bb2[2]); + if (os.x <= g.getWidth()) + r.maxX += 1; + else if (os.y <= g.getHeight()) + r.maxY += 1; + else break; + } + print(".. paint range", r); + return r; +} + +function log2(x) { return Math.log(x) / Math.log(2); } + +function getZoom(qual) { + var z = 16-Math.round(log2(m.scale)); + z += qual; + z -= 0; + if (z < meta.min_zoom) + return meta.min_zoom; + if (z > meta.max_zoom) + return meta.max_zoom; + return z; +} + +function drawDebug(text, perc) { + g.setClipRect(0,0,R.x2,R.y); + g.reset(); + g.setColor(1,1,1).fillRect(0,0,R.x2,R.y); + g.setColor(1,0,0).fillRect(0,0,R.x2*perc,R.y); + g.setColor(0,0,0).setFont("Vector",15); + g.setFontAlign(0,0) + .drawString(text,80,10); + + g.setClipRect(R.x,R.y,R.x2,R.y2); + g.flip(); +} + +function drawAll(qual) { + var zoom = getZoom(qual); + var t1 = getTime(); + + drawDebug("Zoom "+zoom, 0); + + print("Draw all", m.scale, "->", zoom, "q", qual, "at", m.lat, m.lon); + var r = fnames(zoom); + var tiles = (r.maxY-r.minY+1) * (r.maxY-r.minY+1); + var num = 0; + drawDebug("Zoom "+zoom+" tiles "+tiles, 0); + for (y=r.minY; y<=r.maxY; y++) { + for (x=r.minX; x<=r.maxX; x++) { + + for (cnt=0; cnt<1000; cnt++) { + var n ='z'+zoom+'-'+x+'-'+y+'-'+cnt+'.json'; + var gjson = loadVector(n); + if (!gjson) break; + drawVector(gjson, 1); + } + num++; + drawDebug("Zoom "+zoom+" tiles "+num+"/"+tiles, num/tiles); + } + } + g.flip(); + Bangle.drawWidgets(); + print("Load and paint in", getTime()-t1, "sec"); +} + +function initVector() { + var s = readTarFile("delme.mtar", "meta.json"); + meta = JSON.parse(s); + +} + +function introScreen() { + g.reset().clearRect(R); + g.setColor(0,0,0).setFont("Vector",25); + g.setFontAlign(0,0); + g.drawString("SpaceWeaver", 85,35); + g.setColor(0,0,0).setFont("Vector",18); + g.drawString("Vector maps", 85,55); + g.drawString("Zoom "+meta.min_zoom+".."+meta.max_zoom, 85,75); +} + + +m.scale = 76; +m.lat = 50.001; +m.lon = 14.759; + +initVector(); +introScreen(); +emptyMap(); diff --git a/apps/spacew/app.png b/apps/spacew/app.png new file mode 100644 index 000000000..0e52fa316 Binary files /dev/null and b/apps/spacew/app.png differ diff --git a/apps/spacew/metadata.json b/apps/spacew/metadata.json new file mode 100644 index 000000000..51bdb35b8 --- /dev/null +++ b/apps/spacew/metadata.json @@ -0,0 +1,13 @@ +{ "id": "spacew", + "name": "Space Weaver", + "version":"0.01", + "description": "Application for displaying vector maps", + "icon": "app.png", + "readme": "README.md", + "supports" : ["BANGLEJS2"], + "tags": "outdoors,gps,osm", + "storage": [ + {"name":"spacew.app.js","url":"app.js"}, + {"name":"spacew.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/spacew/prep/minitar.js b/apps/spacew/prep/minitar.js new file mode 100755 index 000000000..e07c47049 --- /dev/null +++ b/apps/spacew/prep/minitar.js @@ -0,0 +1,78 @@ +#!/usr/bin/nodejs + +var pc = 1; +var hack = 0; +const hs = require('./heatshrink.js'); + +if (pc) { + fs = require('fs'); + var print=console.log; +} else { + +} + +function writeDir(json) { + json_str = JSON.stringify(json, "", " "); + dirent = '' + json_str.length; + while (dirent.length < 6) + dirent = dirent + ' '; + return dirent + json_str; +} + +function writeTar(tar, dir) { + var h_len = 16; + var cur = h_len; + files = fs.readdirSync(dir); + data = ''; + var directory = ''; + var json = {}; + for (f of files) { + d = fs.readFileSync(dir+f); + cs = d; + //cs = String.fromCharCode.apply(null, hs.compress(d)) + print("Processing", f, cur, d.length, cs.length); + //if (d.length == 42) continue; + data = data + cs; + var f_rec = {}; + f_rec.st = cur; + var len = d.length; + f_rec.si = len; + cur = cur + len; + json[f] = f_rec; + json_str = JSON.stringify(json, "", " "); + if (json_str.length < 16000) + continue; + directory += writeDir(json); + json = {}; + } + directory += writeDir(json); + directory += '-1 '; + + size = cur; + header = '' + size; + while (header.length < h_len) { + header = header+' '; + } + if (!hack) + fs.writeFileSync(tar, header+data+directory); + else + fs.writeFileSync(tar, directory); +} + +function readTarFile(tar, f) { + const st = require('Storage'); + json_off = st.read(tar, 0, 16) * 1; + print(json_off); + json = st.read(tar, json_off, -1); + files = JSON.parse(json); + rec = files[f]; + return st.read(tar, rec.st, rec.si); +} + +if (pc) + writeTar("delme.mtaz", "delme/"); +else { + print(readTarFile("delme.mtar", "ahoj")); + print(readTarFile("delme.mtar", "nazdar")); +} + diff --git a/apps/spacew/prep/prepare.json b/apps/spacew/prep/prepare.json new file mode 100644 index 000000000..33cb21a3c --- /dev/null +++ b/apps/spacew/prep/prepare.json @@ -0,0 +1,18 @@ +{ + "attributes": { + "type": false, + "id": false, + "version": false, + "changeset": false, + "timestamp": false, + "uid": false, + "user": false, + "way_nodes": false, + }, + "format_options": { + }, + "linear_tags": true, + "area_tags": false, + "exclude_tags": [], + "include_tags": [ "place", "name", "landuse", "highway" ] +} diff --git a/apps/spacew/prep/prepare.sh b/apps/spacew/prep/prepare.sh new file mode 100755 index 000000000..ac1db0019 --- /dev/null +++ b/apps/spacew/prep/prepare.sh @@ -0,0 +1,18 @@ +#!/bin/bash +if [ ".$1" == "-f" ]; then + I=/data/gis/osm/dumps/czech_republic-2023-07-24.osm.pbf + #I=/data/gis/osm/dumps/zernovka.osm.bz2 + O=cr.geojson + rm delme.pbf $O + time osmium extract $I --bbox 14.7,49.9,14.8,50.1 -f pbf -o delme.pbf + time osmium export delme.pbf -c prepare.json -o $O + echo "Converting to ascii" + time cstocs utf8 ascii cr.geojson > cr_ascii.geojson + mv -f cr_ascii.geojson delme.json +fi +rm -r delme/; mkdir delme +./split.js +./minitar.js +ls -lS delme/*.json | head -20 +cat delme/* | wc -c +ls -l delme.mtar diff --git a/apps/spacew/prep/split.js b/apps/spacew/prep/split.js new file mode 100755 index 000000000..3d6f81b63 --- /dev/null +++ b/apps/spacew/prep/split.js @@ -0,0 +1,167 @@ +#!/usr/bin/nodejs --max-old-space-size=5500 + +// npm install geojson-vt +// docs: https://github.com/mapbox/geojson-vt +// output format: https://github.com/mapbox/vector-tile-spec/ + +const fs = require('fs'); +const sphm = require('./sphericalmercator.js'); +var split = require('geojson-vt') + +// delme.json needs to be real file, symlink to geojson will not work +console.log("Loading json"); +var gjs = require("./delme.json"); + +function tileToLatLon(x, y, z, x_, y_) { + var [ w, s, e, n ] = merc.bbox(x, y, z); + var lon = (e - w) * (x_ / 4096) + w; + var lat = (n - s) * (1-(y_ / 4096)) + s; + return [ lon, lat ]; +} + +function convGeom(tile, geom) { + var g = []; + for (i = 0; i< geom.length; i++) { + var x = geom[i][0]; + var y = geom[i][1]; + var pos = tileToLatLon(tile.x, tile.y, tile.z, x, y); + g.push(pos); + } + return g; +} + +function zoomPoint(tags) { + var z = 99; + + if (tags.place == "city") z = 4; + if (tags.place == "town") z = 8; + if (tags.place == "village") z = 10; + + return z; +} + +function paintPoint(tags) { + var p = {}; + + if (tags.place == "village") p["marker-color"] = "#ff0000"; + + return p; +} + +function zoomWay(tags) { + var z = 99; + + if (tags.highway == "motorway") z = 7; + if (tags.highway == "primary") z = 9; + if (tags.highway == "secondary") z = 13; + if (tags.highway == "tertiary") z = 14; + if (tags.highway == "unclassified") z = 16; + if (tags.highway == "residential") z = 17; + if (tags.highway == "track") z = 17; + if (tags.highway == "path") z = 17; + if (tags.highway == "footway") z = 17; + + return z; +} + +function paintWay(tags) { + var p = {}; + + if (tags.highway == "motorway" || tags.highway == "primary") /* ok */; + if (tags.highway == "secondary" || tags.highway == "tertiary") p.stroke = "#0000ff"; + if (tags.highway == "tertiary" || tags.highway == "unclassified" || tags.highway == "residential") p.stroke = "#00ff00"; + if (tags.highway == "track") p.stroke = "#ff0000"; + if (tags.highway == "path" || tags.highway == "footway") p.stroke = "#800000"; + + return p; +} + +function writeFeatures(name, feat) +{ + var n = {}; + n.type = "FeatureCollection"; + n.features = feat; + + fs.writeFile(name+'.json', JSON.stringify(n), on_error); +} + +function toGjson(name, d, tile) { + var cnt = 0; + var feat = []; + for (var a of d) { + var f = {}; + var zoom = 99; + var p = {}; + f.properties = a.tags; + f.type = "Feature"; + f.geometry = {}; + if (a.type == 1) { + f.geometry.type = "Point"; + f.geometry.coordinates = convGeom(tile, a.geometry)[0]; + zoom = zoomPoint(a.tags); + p = paintPoint(a.tags); + } else if (a.type == 2) { + f.geometry.type = "LineString"; + f.geometry.coordinates = convGeom(tile, a.geometry[0]); + zoom = zoomWay(a.tags); + p = paintWay(a.tags); + } else { + //console.log("Unknown type", a.type); + } + //zoom -= 4; // Produces way nicer map, at expense of space. + if (tile.z < zoom) + continue; + f.properties = Object.assign({}, f.properties, p); + feat.push(f); + var s = JSON.stringify(feat); + if (s.length > 6000) { + console.log("tile too big, splitting", cnt); + writeFeatures(name+'-'+cnt++, feat); + feat = []; + } + } + writeFeatures(name+'-'+cnt, feat); + return n; +} + +function writeTile(name, d, tile) { + toGjson(name, d, tile) +} + +// By default, precomputes up to z30 +var merc = new sphm({ + size: 256, + antimeridian: true +}); + +console.log("Splitting data"); +var meta = {} +meta.min_zoom = 0; +meta.max_zoom = 17; // HERE + // = 16 ... split3 takes > 30 minutes + // = 13 ... 2 minutes +var index = split(gjs, Object.assign({ + maxZoom: meta.max_zoom, + indexMaxZoom: meta.max_zoom, + indexMaxPoints: 0, + tolerance: 30, +}), {}); +console.log("Producing output"); + +var output = {}; + +function on_error(e) { + if (e) { console.log(e); } +} + +var num = 0; +for (const id in index.tiles) { + const tile = index.tiles[id]; + const z = tile.z; + console.log(num++, ":", tile.x, tile.y, z); + var d = index.getTile(z, tile.x, tile.y).features; + var n = `delme/z${z}-${tile.x}-${tile.y}` ; + writeTile(n, d, tile) +} + +fs.writeFile('delme/meta.json', JSON.stringify(meta), on_error); diff --git a/apps/spacew/prep/stats.sh b/apps/spacew/prep/stats.sh new file mode 100755 index 000000000..6c10ea1b0 --- /dev/null +++ b/apps/spacew/prep/stats.sh @@ -0,0 +1,22 @@ +#!/bin/bash +zoom() { + echo "Zoom $1" + cat delme/z$1-* | wc -c + echo "M..k..." +} + +echo "Total data" +cat delme/* | wc -c +echo "M..k..." +zoom 18 +zoom 17 +zoom 16 +zoom 15 +zoom 14 +zoom 13 +zoom 12 +zoom 11 +zoom 10 +echo "Zoom 1..9" +cat delme/z?-* | wc -c +echo "M..k..." diff --git a/core b/core index 8cf4d0fbf..431a3fb74 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 8cf4d0fbfc310e0d68d616ec779c1888475899a2 +Subproject commit 431a3fb743da5c370729ab748cb2c177e70a345b