Koordinace roznosu letáků (A5) do schránek v ulicích města Jičína
Ulice v Jičíně
Kliknutím na fotku ji zvětšíte (automaticky zkomprimováno pro rychlé uložení)
Hromadné mazání záznamů podle autora/relace a propojení s Google Tabulkou
Mapa je pro dobrovolníky zcela otevřená bez hesla. Tato sekce slouží pouze organizátorům MCH Jičín pro rychlé smazání případného trolla.
Nesprávný organizátorský PIN.
Každému návštěvníkovi se při vstupu na mapu vygeneruje skrytý podpis prohlížeče (sessionId). I když troll u každé vesnice napíše jiné vymyšlené jméno, systém ho zde seskupí na jedno místo a vy **jediným kliknutím smažete všechny jeho záznamy najednou**.
Pokud někdo před chvílí naklikal nesmysly, můžete jedním kliknutím vrátit do stavu „Čeká na vyvěšení“ všechny obce změněné v posledních minutách:
Kdykoliv můžete jedním kliknutím vynulovat všechny obce zpět na 0 % (Čeká na vyvěšení).
Jakmile sem vložíte URL adresu svého skriptu z Google Tabulky, mapa se automaticky přepne z lokálního režimu na živou synchronizaci pro všechny návštěvníky z Facebooku.
DEFAULT_GOOGLE_SCRIPT_URL v kódu této stránky před nahráním na web).function ensureColumns(sheet, minCols) {
var maxCols = sheet.getMaxColumns();
if (maxCols < minCols) {
sheet.insertColumnsAfter(maxCols, minCols - maxCols);
}
}
function isValidPhoto(val) {
if (!val) return false;
var s = String(val).trim();
return s.indexOf('data:image/') === 0 || s.indexOf('http://') === 0 || s.indexOf('https://') === 0;
}
function getPhotoFingerprint(p) {
var s = String(p || '').trim();
if (s.length <= 400) return s;
var mid = Math.floor(s.length / 2);
return s.length + ':' + s.substring(50, 180) + ':' + s.substring(mid, mid + 130) + ':' + s.substring(s.length - 130);
}
function addUniquePhoto(archiveArr, p) {
if (!isValidPhoto(p)) return;
var s = String(p).trim();
var fp = getPhotoFingerprint(s);
for (var i = 0; i < archiveArr.length; i++) {
if (getPhotoFingerprint(archiveArr[i]) === fp) {
return;
}
}
archiveArr.push(s);
}
function parseTimestamp(val) {
if (!val) return 0;
if (val instanceof Date) return val.getTime();
var t = new Date(String(val)).getTime();
return isNaN(t) ? 0 : t;
}
function formatIsoTimestamp(val) {
if (!val) return '';
if (val instanceof Date) return val.toISOString();
return String(val).trim();
}
function sjednotitListyDoObce() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets();
var targetSheet = ss.getSheetByName('Obce');
if (
sheets.length === 1 &&
targetSheet &&
targetSheet.getLastRow() > 0 &&
targetSheet.getMaxColumns() >= 9 &&
String(targetSheet.getRange(1, 9).getValue() || '').indexOf('ArchivFoto') === 0
) {
return targetSheet;
}
var mergedMap = {};
var orderIds = [];
var maxArchiveLen = 1;
for (var sIdx = 0; sIdx < sheets.length; sIdx++) {
var sh = sheets[sIdx];
if (sh.getLastRow() === 0) continue;
var rows = sh.getDataRange().getValues();
var header = rows[0] || [];
var isLegacyColH = String(header[7] || '').toLowerCase().indexOf('foto') !== -1;
for (var i = 1; i < rows.length; i++) {
var id = rows[i][0] ? String(rows[i][0]).trim() : '';
if (!id || id.toLowerCase() === 'id') continue;
var name = rows[i][1] ? String(rows[i][1]).trim() : '';
var status = rows[i][2] ? String(rows[i][2]).trim() : 'waiting';
var volunteer = rows[i][3] ? String(rows[i][3]).trim() : '';
var note = (rows[i][4] !== undefined && rows[i][4] !== null) ? String(rows[i][4]) : '';
var rawCol5 = rows[i][5];
var rawCol6 = rows[i][6];
var rawCol7 = rows[i][7];
var photo = '';
var updatedAt = '';
var sessionId = '';
if (isLegacyColH) {
updatedAt = formatIsoTimestamp(rawCol5);
sessionId = String(rawCol6 || '').trim();
photo = isValidPhoto(rawCol7) ? String(rawCol7).trim() : '';
} else {
photo = isValidPhoto(rawCol5) ? String(rawCol5).trim() : (isValidPhoto(rawCol7) ? String(rawCol7).trim() : '');
updatedAt = formatIsoTimestamp(rawCol6);
sessionId = isValidPhoto(rawCol7) ? '' : String(rawCol7 || '').trim();
}
var rowArchive = [];
addUniquePhoto(rowArchive, photo);
if (!isLegacyColH && isValidPhoto(rawCol7)) {
addUniquePhoto(rowArchive, rawCol7);
}
for (var c = 8; c < rows[i].length; c++) {
addUniquePhoto(rowArchive, rows[i][c]);
}
if (!mergedMap[id]) {
orderIds.push(id);
mergedMap[id] = {
id: id,
name: name,
status: status,
volunteer: volunteer,
note: note,
photo: photo,
updatedAt: updatedAt,
sessionId: sessionId,
archive: rowArchive
};
} else {
var ex = mergedMap[id];
for (var a = 0; a < rowArchive.length; a++) {
addUniquePhoto(ex.archive, rowArchive[a]);
}
var exTime = parseTimestamp(ex.updatedAt);
var newTime = parseTimestamp(updatedAt);
if (newTime >= exTime) {
ex.name = name || ex.name;
ex.status = status || ex.status;
ex.volunteer = volunteer !== '' ? volunteer : ex.volunteer;
ex.note = note !== '' ? note : ex.note;
ex.photo = photo || ex.photo;
ex.updatedAt = updatedAt || ex.updatedAt;
ex.sessionId = sessionId || ex.sessionId;
} else {
if (!ex.name && name) ex.name = name;
if (!ex.photo && photo) ex.photo = photo;
if (!ex.note && note) ex.note = note;
if (!ex.volunteer && volunteer) ex.volunteer = volunteer;
}
}
}
}
if (!targetSheet) {
targetSheet = sheets[0];
targetSheet.setName('Obce');
}
orderIds.sort(function(a, b) {
if (a === '__map_geo_config__') return -1;
if (b === '__map_geo_config__') return 1;
return a.localeCompare(b, 'cs');
});
var outputRows = [];
for (var k = 0; k < orderIds.length; k++) {
var rec = mergedMap[orderIds[k]];
if (!rec.photo && rec.status !== 'waiting' && rec.archive.length > 0) {
rec.photo = rec.archive[rec.archive.length - 1];
}
if (rec.archive.length > maxArchiveLen) {
maxArchiveLen = rec.archive.length;
}
var isEmptyWaiting = (rec.status === 'waiting' && !rec.volunteer && !rec.note && !rec.photo && rec.archive.length === 0);
if (!isEmptyWaiting) {
outputRows.push([
rec.id,
rec.name,
rec.status,
rec.volunteer,
rec.note,
rec.photo,
rec.updatedAt,
rec.sessionId
].concat(rec.archive));
}
}
var totalCols = 8 + maxArchiveLen;
ensureColumns(targetSheet, totalCols);
targetSheet.clearContents();
var headerRow = ['id', 'name', 'status', 'volunteer', 'note', 'photo', 'updatedAt', 'sessionId'];
for (var h = 1; h <= maxArchiveLen; h++) {
headerRow.push('ArchivFoto_' + h);
}
var allValues = [headerRow];
for (var r = 0; r < outputRows.length; r++) {
var rowArr = outputRows[r];
while (rowArr.length < totalCols) {
rowArr.push('');
}
allValues.push(rowArr);
}
targetSheet.getRange(1, 1, allValues.length, totalCols).setValues(allValues);
targetSheet.setFrozenRows(1);
for (var d = sheets.length - 1; d >= 0; d--) {
if (sheets[d].getName() !== 'Obce') {
try {
ss.deleteSheet(sheets[d]);
} catch (err) {}
}
}
return targetSheet;
}
function doGet() {
var sheet = sjednotitListyDoObce();
if (sheet.getLastRow() <= 1) {
return ContentService.createTextOutput(JSON.stringify({ status: 'ok', data: {} }))
.setMimeType(ContentService.MimeType.JSON);
}
var rows = sheet.getDataRange().getValues();
var data = {};
for (var i = 1; i < rows.length; i++) {
var id = rows[i][0] ? String(rows[i][0]).trim() : '';
if (!id) continue;
var status = rows[i][2] ? String(rows[i][2]).trim() : 'waiting';
var volunteer = rows[i][3] ? String(rows[i][3]).trim() : '';
var note = (rows[i][4] !== undefined && rows[i][4] !== null) ? String(rows[i][4]) : '';
var rawPhoto = rows[i][5];
var photo = isValidPhoto(rawPhoto) ? String(rawPhoto).trim() : '';
var updatedAt = formatIsoTimestamp(rows[i][6]);
var sessionId = String(rows[i][7] || '').trim();
if (status !== 'waiting' || volunteer || note || photo) {
data[id] = {
id: id,
name: rows[i][1] ? String(rows[i][1]) : '',
status: status,
volunteer: volunteer,
author: volunteer,
note: note,
photoUrl: photo,
updatedAt: updatedAt,
sessionId: sessionId
};
}
}
return ContentService.createTextOutput(JSON.stringify({ status: 'ok', data: data }))
.setMimeType(ContentService.MimeType.JSON);
}
function doPost(e) {
var lock = LockService.getScriptLock();
lock.waitLock(15000);
try {
var sheet = sjednotitListyDoObce();
if (sheet.getLastRow() === 0) {
ensureColumns(sheet, 9);
sheet.appendRow(['id', 'name', 'status', 'volunteer', 'note', 'photo', 'updatedAt', 'sessionId', 'ArchivFoto_1']);
sheet.setFrozenRows(1);
}
var payload = JSON.parse(e.postData.contents);
var changes = payload.changes || [];
if (changes.length === 0 && payload.record) {
changes = [payload.record];
} else if (changes.length === 0 && payload.item) {
changes = [payload.item];
} else if (changes.length === 0 && payload.id) {
changes = [payload];
} else if (changes.length === 0 && (payload.state || payload.fullState)) {
var st = payload.state || payload.fullState;
changes = Object.keys(st).map(function(k) { return st[k]; });
}
var rows = sheet.getDataRange().getValues();
var rowMap = {};
for (var i = 1; i < rows.length; i++) {
var rId = rows[i][0] ? String(rows[i][0]).trim() : '';
if (rId) {
rowMap[rId] = i + 1;
}
}
var rowsToDelete = [];
var maxArchiveLen = 1;
changes.forEach(function(item) {
if (!item || !item.id) return;
var itemId = String(item.id).trim();
var rIdx = rowMap[itemId];
var existingRow = (rIdx && rows[rIdx - 1]) ? rows[rIdx - 1] : null;
var archive = [];
if (existingRow) {
addUniquePhoto(archive, existingRow[5]);
addUniquePhoto(archive, existingRow[7]);
for (var c = 8; c < existingRow.length; c++) {
addUniquePhoto(archive, existingRow[c]);
}
}
if (Array.isArray(item.photoArchive)) {
for (var a = 0; a < item.photoArchive.length; a++) {
addUniquePhoto(archive, item.photoArchive[a]);
}
}
var rawCurrentPhoto = item.photoUrl !== undefined ? item.photoUrl : (item.photo || '');
var existingActivePhoto = (existingRow && isValidPhoto(existingRow[5])) ? String(existingRow[5]).trim() : '';
var currentPhoto = '';
if (isValidPhoto(rawCurrentPhoto)) {
currentPhoto = String(rawCurrentPhoto).trim();
} else if (!item.photoCleared && payload.action !== 'resetBatch' && existingActivePhoto) {
currentPhoto = existingActivePhoto;
}
addUniquePhoto(archive, currentPhoto);
if (archive.length > maxArchiveLen) {
maxArchiveLen = archive.length;
}
var volunteerName = item.author !== undefined ? item.author : (item.volunteer || '');
var statusVal = item.status || 'waiting';
var noteVal = item.note || '';
var updatedVal = item.updatedAt || new Date().toISOString();
var sessionVal = item.sessionId || '';
var villageName = item.name || (existingRow ? existingRow[1] : '');
var isEmptyWaiting = (statusVal === 'waiting' && !volunteerName && !noteVal && !currentPhoto && archive.length === 0);
var rowValues = [
itemId,
villageName,
statusVal,
volunteerName,
noteVal,
currentPhoto,
updatedVal,
sessionVal
].concat(archive);
if (rIdx) {
if (isEmptyWaiting) {
rowsToDelete.push(rIdx);
} else {
var maxCols = Math.max(existingRow ? existingRow.length : 8, rowValues.length);
while (rowValues.length < maxCols) {
rowValues.push('');
}
ensureColumns(sheet, rowValues.length);
sheet.getRange(rIdx, 1, 1, rowValues.length).setValues([rowValues]);
}
} else if (!isEmptyWaiting) {
ensureColumns(sheet, rowValues.length);
var newRowIdx = sheet.getLastRow() + 1;
sheet.getRange(newRowIdx, 1, 1, rowValues.length).setValues([rowValues]);
rowMap[itemId] = newRowIdx;
}
});
if (sheet.getLastRow() > 0) {
ensureColumns(sheet, 8 + maxArchiveLen);
for (var h = 1; h <= maxArchiveLen; h++) {
if (!sheet.getRange(1, 8 + h).getValue()) {
sheet.getRange(1, 8 + h).setValue('ArchivFoto_' + h);
}
}
}
rowsToDelete.sort(function(a, b) { return b - a; }).forEach(function(rIdx) {
sheet.deleteRow(rIdx);
});
return ContentService.createTextOutput(JSON.stringify({ status: 'ok' }))
.setMimeType(ContentService.MimeType.JSON);
} finally {
lock.releaseLock();
}
}
Zkopírujte si připravený společný motivační text s aktuální statistikou obou map (plakáty v obcích + letáky v Jičíně) a vložte ho na facebookovou stránku Milion chvilek – Jičín:
Text do prvního komentáře pod příspěvkem (s odkazy na obě mapy):
Vyberte si volnou ulici (🔴 Červená – Čeká na roznos): Podívejte se do mapy Jičína nebo vyhledejte ulici v abecedním seznamu vpravo.
Zamluvte si ji (🟠 Oranžová – Zamluveno): Klikněte na ulici a zvolte „Zamluveno“. Ostatní dobrovolníci hned uvidí, že si tuto ulici berete na starost, a nepůjdou na stejné místo.
Označte jako roznesené (🟢 Zelená – Rozneseno): Jakmile letáky (formát A5) vhodíte do schránek v dané ulici, přepněte ulici na „Rozneseno!“. Do poznámky můžete připsat např. kolik letáků bylo potřeba nebo zda je hotová celá ulice. Společně rozsvítíme celý Jičín do zelena!
Vytiskněte si letáky (A5) sami doma nebo v práci, případně si hotové výtisky vyzvedněte v Jičíně