Merge remote-tracking branch 'upstream/master'

master
Hugh Barney 2023-09-04 22:38:53 +01:00
commit 81f382e5d0
78 changed files with 7000 additions and 1473 deletions

View File

@ -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

View File

@ -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);

View File

@ -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"},

1
apps/bblobface/ChangeLog Normal file
View File

@ -0,0 +1 @@
1.00: Initial release of Bangle Blobs Clock!

35
apps/bblobface/README.md Normal file
View File

@ -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.

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+HGm56+5BQ4JBAJItXAAoMMCJQAPJ5pfhJApPQL65HHKIbTU2nXAAu0I5xQNBo4tC2gAFGIxHIL5oNGEoItGGIgwDL6oMGFxgwFL6oVFFxwwEL7YuPGARfVBYwvUL6YLGL84THL84KHL7YHCL6AeBFx+0JggAGLx4wQFwa3DAIwvHNJQwMFwhgIEQ7ILGAYxHBAQWJADUeFAIAEjwtnjwAFGMglBFowxEGA/XgrgICJouMGA4aBAIgvMB4ouOGAouGMZgNGFx4wCPQ5hMN44vTK44wLNo5fUcRwuHL67iOHAxfhFxYJBBooeBFx8ecRY4KBowwOFxDgHM5BtHGBguZfhIkBGI4ICFyILFAIxBHAAoOGXIgLHBowBGFo0FAAoxHFxhfPAoQAJCIguNGxRtGABYpDQB72LFxwwEcCJfJFx4wCL7gvTADYv/F/4APYoQuOaoYwpFz4wOF0IwDGI4ICF0IxFAAgtFA="))

768
apps/bblobface/app.js Normal file
View File

@ -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();
}

BIN
apps/bblobface/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 691 B

View File

@ -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}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -28,11 +28,16 @@ function readFile(input) {
for(let i=0; i<input.files.length; i++) {
const reader = new FileReader();
reader.addEventListener("load", () => {
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);

View File

@ -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.

View File

@ -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)

View File

@ -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) {

View File

@ -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"],

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -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();
},
},

View File

@ -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

View File

@ -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 :

View File

@ -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 ?

File diff suppressed because one or more lines are too long

BIN
apps/gipy/heights.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -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",

View File

@ -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;

View File

@ -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);
};

Binary file not shown.

View File

@ -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;

View File

@ -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,

BIN
apps/gipy/shot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -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

View File

@ -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);

View File

@ -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",

View File

@ -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
0.03: Fix invalid use of Bangle.setUI
0.04: Fix app crashing when new message arrives

View File

@ -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?'<callback>':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: "<unknown>", artist: "<unknown>", 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)));
}
}

View File

@ -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",

View File

@ -0,0 +1,2 @@
1.0: first working version of App
1.1: bugfix (enable settings page)

20
apps/nightwatch/README.md Normal file
View File

@ -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/)

View File

@ -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"}]
}

View File

@ -0,0 +1,6 @@
require("Storage").write("nightwatch.info",{
"id":"nightwatch",
"name":"nightwatch",
"src":"nightwatch.app.js",
"icon":"nightwatch.icon.png"
});

View File

@ -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();

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4MA///ospETUQAgc//gFDv4FF/wFP/4FF/5PCgIFChF/AoWA/1/+YFBx/+g4EBAAPAFAIEBEgUDBQYAN/E/AgQvDDoXHDocH4wFDgf8v4RCDooAMj/4AoZcBcM8DOAgFFgJSDAqQAhA=="))

Binary file not shown.

After

Width:  |  Height:  |  Size: 959 B

View File

@ -0,0 +1,6 @@
require("Storage").write("nightwatch.info",{
"id":"nightwatch",
"name":"The Nightwatch",
"src":"nightwatch.app.js",
"icon":"nightwatch.icon.png"
});

View File

@ -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();
}
},
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1 @@
0.01: Initial version

View File

@ -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

View File

@ -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=="))

583
apps/rebbleagenda/app.js Normal file
View File

@ -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; // <20>=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();
}

BIN
apps/rebbleagenda/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 B

View File

@ -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"}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -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();
},
}
});
});

View File

@ -16,14 +16,18 @@ function readFile(input) {
for(let i=0; i<input.files.length; i++) {
const reader = new FileReader();
reader.addEventListener("load", () => {
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;

5
apps/sokoban/ChangeLog Normal file
View File

@ -0,0 +1,5 @@
0.01: Initial code
0.02:
* Fix for last level offsets parsing
* Fix for title display

20
apps/sokoban/README.md Normal file
View File

@ -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

2
apps/sokoban/TODO Normal file
View File

@ -0,0 +1,2 @@
- background
- win screen + final win screen

1
apps/sokoban/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwhC/AH4A/ABMN7oXV7vd6AuVAAIXaAwYAEC75fGC6KPFC58BiMQJxAXLiIABDAcBigXRAAIFDC5pGBAAwvOCQYbDiBfOLwgYBO54qER6QXDexIXJRggXRIxIXpb4wXMLwYvTdIinSC4gYFC5xiIC54YDC54SBIQZHRC4gcFC5hyEC4KPPLIrZGC5IWCLwgXPUApeGC5KfGC6DnGIwwXLB4gXQgI/FAwy/MAH4A/ABgA=="))

471
apps/sokoban/app.js Normal file
View File

@ -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();

View File

@ -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"}
]
}

BIN
apps/sokoban/soko.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

File diff suppressed because it is too large Load Diff

BIN
apps/sokoban/sokoban.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 863 B

43
apps/spacew/README.md Normal file
View File

@ -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.

2
apps/spacew/app-icon.js Normal file
View File

@ -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="))

620
apps/spacew/app.js Normal file
View File

@ -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<mx && ttx<map.w; x+=s,ttx++)
for (var y=oy,tty=ty;y<my && tty<map.h;y+=s,tty++) {
o.frame = ttx+(tty*map.w);
g.drawImage(img,x,y,o);
}
});
};
/// Convert lat/lon to pixels on the screen
exports.latLonToXY = function(lat, lon) {
var p = Bangle.project({lat:m.lat,lon:m.lon});
var q = Bangle.project({lat:lat, lon:lon});
var cx = g.getWidth()/2;
var cy = g.getHeight()/2;
return {
x : (q.x-p.x)/m.scale + cx,
y : cy - (q.y-p.y)/m.scale
};
};
/// Given an amount to scroll in pixels on the screen, adjust the lat/lon of the map to match
exports.scroll = function(x,y) {
var a = Bangle.project({lat:m.lat,lon:m.lon});
var b = Bangle.project({lat:m.lat+1,lon:m.lon+1});
this.lon += x * m.scale / (a.x-b.x);
this.lat -= y * m.scale / (a.y-b.y);
};
/*
Copyright (c) 2011, Development Seed
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
- Neither the name "Development Seed" nor the names of its contributors may be
used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
// Convert lon lat to screen pixel value
//
// - `ll` {Array} `[lon, lat]` array of geographic coordinates.
// - `zoom` {Number} zoom level.
function px(ll, zoom) {
var size = 256 * Math.pow(2, zoom);
var d = size / 2;
var bc = (size / 360);
var cc = (size / (2 * Math.PI));
var ac = size;
var D2R = Math.PI / 180;
var f = Math.min(Math.max(Math.sin(D2R * ll[1]), -0.9999), 0.9999);
var x = d + ll[0] * bc;
var y = d + 0.5 * Math.log((1 + f) / (1 - f)) * -cc;
var expansion = 1;
(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();

BIN
apps/spacew/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

13
apps/spacew/metadata.json Normal file
View File

@ -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}
]
}

78
apps/spacew/prep/minitar.js Executable file
View File

@ -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"));
}

View File

@ -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" ]
}

18
apps/spacew/prep/prepare.sh Executable file
View File

@ -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

167
apps/spacew/prep/split.js Executable file
View File

@ -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);

22
apps/spacew/prep/stats.sh Executable file
View File

@ -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..."

2
core

@ -1 +1 @@
Subproject commit 8cf4d0fbfc310e0d68d616ec779c1888475899a2
Subproject commit 431a3fb743da5c370729ab748cb2c177e70a345b