Koordinace vyvěšování plakátů v okruhu 20 km kolem Jičína
Vzdálenost od Jičína: 0 km
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();
}
}
Příspěvek na facebookovou stránku Milion chvilek – Jičín připravíte ve 3 krocích. Text obsahuje aktuální statistiku obou map (plakáty v obcích + letáky v Jičíně).
1 Zkopírujte text příspěvku
2 Přiložte k příspěvku obrázek s adresou mapy
Facebook nám omezuje odkazy v příspěvcích, proto je adresa mapa.milionchvilekjicin.cz velkým písmem přímo na obrázku – i s QR kódy obou map. Každý si ji přečte na první pohled.
3 Po zveřejnění vložte první komentář s odkazy
🔳 QR kódy ke stažení (na plakáty, letáky, nástěnky…)
📍 Plakáty v obcích
📬 Letáky v Jičíně
Vyberte si volnou obec (🔴 Červená): Podívejte se do mapy nebo vyhledejte vesnici v seznamu vpravo, kam máte cestu.
Zamluvte si ji (🟠 Oranžová): Klikněte na obec a zvolte „Zamlouvám si“. Ostatní dobrovolníci uvidí, že tam už někdo míří, a nepojedou na stejné místo.
Označte jako vyvěšené (🟢 Zelená): Jakmile plakáty v obci vyvěsíte (např. upevníte sponkovačkou), přepněte obec na „Mám vyvěšeno!“ a můžete přidat krátkou poznámku (např. kde plakát visí). V ideálním případě nahrajte fotku vývěsky s vyvěšeným plakátem. Společně rozsvítíme celé Jičínsko do zelena!
Bez plochy / Placený výlep (⚪ Šedá): Pokud v obci není vývěska / plakátovací plocha nebo je tam pouze placený výlep, zvolte možnost „Bez plochy / Placený výlep“. Ostatní dobrovolníci tak hned uvidí, že tam jezdit nemusí.
Vytiskněte si plakáty sami doma nebo v práci, případně si hotové výtisky vyzvedněte v Jičíně