Merge remote-tracking branch 'upstream/master'
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
1.00: Initial release of Bangle Blobs Clock!
|
||||
|
|
@ -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.
|
||||
|
||||

|
||||

|
||||
|
||||
## 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.
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwxH+HGm56+5BQ4JBAJItXAAoMMCJQAPJ5pfhJApPQL65HHKIbTU2nXAAu0I5xQNBo4tC2gAFGIxHIL5oNGEoItGGIgwDL6oMGFxgwFL6oVFFxwwEL7YuPGARfVBYwvUL6YLGL84THL84KHL7YHCL6AeBFx+0JggAGLx4wQFwa3DAIwvHNJQwMFwhgIEQ7ILGAYxHBAQWJADUeFAIAEjwtnjwAFGMglBFowxEGA/XgrgICJouMGA4aBAIgvMB4ouOGAouGMZgNGFx4wCPQ5hMN44vTK44wLNo5fUcRwuHL67iOHAxfhFxYJBBooeBFx8ecRY4KBowwOFxDgHM5BtHGBguZfhIkBGI4ICFyILFAIxBHAAoOGXIgLHBowBGFo0FAAoxHFxhfPAoQAJCIguNGxRtGABYpDQB72LFxwwEcCJfJFx4wCL7gvTADYv/F/4APYoQuOaoYwpFz4wOF0IwDGI4ICF0IxFAAgtFA="))
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
After Width: | Height: | Size: 691 B |
|
|
@ -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}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||

|
||||

|
||||

|
||||

|
||||
|
||||
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)
|
||||
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 2.6 KiB |
|
|
@ -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();
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||

|
||||
|
||||
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 :
|
||||
|
|
|
|||
|
|
@ -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 ?
|
||||
|
|
|
|||
3043
apps/gipy/app.js
|
After Width: | Height: | Size: 3.5 KiB |
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 4.6 KiB |
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
1.0: first working version of App
|
||||
1.1: bugfix (enable settings page)
|
||||
|
|
@ -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:
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
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/)
|
||||
|
|
@ -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"}]
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
require("Storage").write("nightwatch.info",{
|
||||
"id":"nightwatch",
|
||||
"name":"nightwatch",
|
||||
"src":"nightwatch.app.js",
|
||||
"icon":"nightwatch.icon.png"
|
||||
});
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEw4MA///ospETUQAgc//gFDv4FF/wFP/4FF/5PCgIFChF/AoWA/1/+YFBx/+g4EBAAPAFAIEBEgUDBQYAN/E/AgQvDDoXHDocH4wFDgf8v4RCDooAMj/4AoZcBcM8DOAgFFgJSDAqQAhA=="))
|
||||
|
After Width: | Height: | Size: 959 B |
|
|
@ -0,0 +1,6 @@
|
|||
require("Storage").write("nightwatch.info",{
|
||||
"id":"nightwatch",
|
||||
"name":"The Nightwatch",
|
||||
"src":"nightwatch.app.js",
|
||||
"icon":"nightwatch.icon.png"
|
||||
});
|
||||
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
|
@ -0,0 +1 @@
|
|||
0.01: Initial version
|
||||
|
|
@ -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.
|
||||
|
||||
  
|
||||
|
||||
## 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
|
||||
|
|
@ -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=="))
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
After Width: | Height: | Size: 479 B |
|
|
@ -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"}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
|
@ -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();
|
||||
},
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
0.01: Initial code
|
||||
0.02:
|
||||
* Fix for last level offsets parsing
|
||||
* Fix for title display
|
||||
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# Sokoban
|
||||
|
||||
Classic Sokoban game.
|
||||
|
||||
Tap screen at bottom/top/left/right to push boxes into their destinations.
|
||||
Swipe to undo.
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
- background
|
||||
- win screen + final win screen
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwhC/AH4A/ABMN7oXV7vd6AuVAAIXaAwYAEC75fGC6KPFC58BiMQJxAXLiIABDAcBigXRAAIFDC5pGBAAwvOCQYbDiBfOLwgYBO54qER6QXDexIXJRggXRIxIXpb4wXMLwYvTdIinSC4gYFC5xiIC54YDC54SBIQZHRC4gcFC5hyEC4KPPLIrZGC5IWCLwgXPUApeGC5KfGC6DnGIwwXLB4gXQgI/FAwy/MAH4A/ABgA=="))
|
||||
|
|
@ -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();
|
||||
|
|
@ -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"}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 863 B |
|
|
@ -0,0 +1,43 @@
|
|||
# Space Weaver 
|
||||
|
||||
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.
|
||||
|
|
@ -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="))
|
||||
|
||||
|
|
@ -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();
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
|
|
@ -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}
|
||||
]
|
||||
}
|
||||
|
|
@ -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"));
|
||||
}
|
||||
|
||||
|
|
@ -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" ]
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
|
|
@ -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
|
||||