Przejdź do głównej treści

Kalkulator Single Speed

Grafika SVG Rower

Kalkulator optymalnego przełożenia Single Speed

Chcesz wiedzieć, jaka tylna zębatka będzie najlepsza dla Twojego roweru Single Speed?
Skorzystaj z naszego kalkulatora i sprawdź, jakie przełożenie sprawdzi się najlepiej w zależności od trasy, którą planujesz pokonać.

Jak to działa?

  1. Wprowadź dane swojego roweru i wagi kolarza – wystarczy zrobić to raz.
  2. Dodaj odcinki trasy:
    • Dystans w kilometrach,
    • Rodzaj terenu (płaski, pagórkowaty, stromy),
    • Średnie nachylenie w procentach.
  3. Możesz dodać dowolną liczbę odcinków – np. 10 km płaskiego, 5 km pod górę, 3 km stromego podjazdu.
  4. Podaj liczbę zębów przedniej zębatki (np. 42).
  5. Kalkulator wyliczy optymalną tylną zębatkę oraz poda najbliższy dostępny wolnobieg (np. 16T, 18T, 20T).

Algorytm korzysta z uproszczonego modelu, który uwzględnia:

  • średnie nachylenie trasy,
  • udział poszczególnych rodzajów terenu w całej trasie,
  • wagę roweru i kolarza (do korekty trudności jazdy).

FAQ

Czy kalkulator poda mi jedną idealną wartość?
Nie do końca – poda wartość obliczoną oraz najbliższy dostępny wolnobieg. To sugestia, a nie absolutna odpowiedź.

Co jeśli jeżdżę w różnych warunkach (miasto + góry)?
Możesz dodać różne trasy do tabeli i sprawdzić, jakie przełożenia są sugerowane w każdym scenariuszu.

Czy wynik nadaje się do ścigania?
Kalkulator jest przybliżonym narzędziem – dobór przełożenia do wyścigów zależy też od stylu jazdy, kadencji i preferencji.

Jakie przełożenie jest najczęściej wybierane?
Na szosie popularne są zestawy typu 42/16 lub 46/17. W trudniejszym terenie warto rozważyć większą tylną zębatkę (np. 18–20T).

Spróbuj teraz!
Wprowadź swoje dane i zobacz, jakie przełożenie Single Speed będzie najlepsze na Twoją trasę.

Parametry roweru i kolarza
Odcinki trasy
# Dystans (km) Teren Nachylenie (%)

Wynik pojawi się tutaj...

Kod po stronie przeglądarki

<div id="app">
  <!-- Parametry roweru i kolarza -->
  <div class="card mb-3">
    <div class="card-header">Parametry roweru i kolarza</div>
    <div class="card-body">
      <div class="row g-2">
        <div class="col-sm-2">
          <label for="bikeWeight" class="form-label small">Waga roweru (kg)</label>
          <input type="number" class="form-control" id="bikeWeight" value="8.5">
        </div>
        <div class="col-sm-2">
          <label for="riderWeight" class="form-label small">Waga kolarza (kg)</label>
          <input type="number" class="form-control" id="riderWeight" value="75">
        </div>
        <div class="col-sm-2">
          <label for="wheelSize" class="form-label small">Średnica koła (mm)</label>
          <input type="number" class="form-control" id="wheelSize" value="700">
        </div>
        <div class="col-sm-2">
          <label for="tireWidth" class="form-label small">Szerokość opony (mm)</label>
          <input type="number" class="form-control" id="tireWidth" value="28">
        </div>
        <div class="col-sm-2">
          <label for="bikeType" class="form-label small">Typ roweru</label>
          <select class="form-select" id="bikeType">
            <option value="road">Szosa</option>
            <option value="gravel">Szuter</option>
            <option value="urban">Miasto</option>
          </select>
        </div>
      </div>
    </div>
  </div>

  <!-- Tabela odcinków -->
  <div class="card mb-3">
    <div class="card-header">Odcinki trasy</div>
    <div class="card-body">
      <div class="row mb-2">
        <div class="col-sm-3">
          <input type="number" step="0.1" class="form-control" id="distance" placeholder="Dystans (km)" aria-label="Dystans trasy">
        </div>
        <div class="col-sm-3">
          <select class="form-select" id="terrain" aria-label="Rodzaj terenu">
            <option value="flat">Płaski</option>
            <option value="hilly">Pagórkowaty</option>
            <option value="steep">Stromy</option>
          </select>
        </div>
        <div class="col-sm-3">
          <input type="number" step="0.1" class="form-control" id="slope" placeholder="Nachylenie (%)" aria-label="Nachylenie terenu">
        </div>
        <div class="col-sm-3">
          <button class="btn btn-primary w-100" id="addSegmentBtn">Dodaj odcinek</button>
        </div>
      </div>

      <table class="table table-sm table-striped">
        <thead class="table-dark">
          <tr>
            <th>#</th>
            <th>Dystans (km)</th>
            <th>Teren</th>
            <th>Nachylenie (%)</th>
          </tr>
        </thead>
        <tbody id="segmentsBody"></tbody>
      </table>

      <div class="d-flex gap-2">
        <button class="btn btn-secondary" id="loadDefaultSegments">Wczytaj przykładową trasę</button>
        <button class="btn btn-outline-danger" id="clearSegments">Wyczyść odcinki</button>
      </div>
    </div>
  </div>

  <!-- Obliczenia przełożenia -->
  <div class="card">
    <div class="card-body">
      <div class="row align-items-center">
        <div class="col-sm-4">
          <label for="frontTeeth" class="form-label small">Zęby przedniej zębatki</label>
          <input type="number" class="form-control" id="frontTeeth" value="42">
        </div>
        <div class="col-sm-4">
          <button class="btn btn-success mt-4" id="calcBtn">Oblicz optymalne przełożenie</button>
        </div>
      </div>
      <div class="mt-3">
        <h3 class="h4" id="result">Wynik pojawi się tutaj...</h3>
        <pre id="details" style="display:none; background:#f8f9fa; padding:10px; border-radius:6px;"></pre>
      </div>
    </div>
  </div>
</div>
<script>
  let segmentCounter = 0;
  const TERRAIN_LABEL = { flat: 'Płaski', hilly: 'Pagórkowaty', steep: 'Stromy' };

  const DEFAULT_SEGMENTS = [
    { distance: 10, terrain: 'flat', slope: 0 },
    { distance: 5, terrain: 'hilly', slope: 4 },
    { distance: 3, terrain: 'steep', slope: 8 },
    { distance: 12, terrain: 'flat', slope: 1 }
  ];

  function addSegment(data) {
    const distance = data ? parseFloat(data.distance) : parseFloat(document.getElementById('distance').value);
    const terrain = data ? data.terrain : document.getElementById('terrain').value;
    const slope = data ? parseFloat(data.slope) : parseFloat(document.getElementById('slope').value);

    if (Number.isNaN(distance) || Number.isNaN(slope)) {
      alert('Podaj poprawne wartości dystansu i nachylenia.');
      return;
    }

    segmentCounter++;
    const tr = document.createElement('tr');
    tr.dataset.distance = distance;
    tr.dataset.terrain = terrain;
    tr.dataset.slope = slope;

    tr.innerHTML = `
      <td>${segmentCounter}</td>
      <td>${distance.toFixed(1)}</td>
      <td>${TERRAIN_LABEL[terrain]}</td>
      <td>${slope.toFixed(1)}</td>
    `;

    document.getElementById('segmentsBody').appendChild(tr);
  }

  function loadDefaultSegments() {
    clearSegments();
    DEFAULT_SEGMENTS.forEach(seg => addSegment(seg));
  }

  function clearSegments() {
    document.getElementById('segmentsBody').innerHTML = '';
    segmentCounter = 0;
  }

  function calculateGear() {
    const frontTeeth = parseInt(document.getElementById('frontTeeth').value, 10);
    const rows = Array.from(document.querySelectorAll('#segmentsBody tr'));

    if (!frontTeeth || frontTeeth <= 0) {
      alert('Wprowadź poprawną liczbę zębów przedniej zębatki.');
      return;
    }

    if (rows.length === 0) {
      alert('Dodaj odcinki trasy.');
      return;
    }

    // dane roweru i kolarza
    const bikeWeight = parseFloat(document.getElementById('bikeWeight').value) || 0;
    const riderWeight = parseFloat(document.getElementById('riderWeight').value) || 0;
    const wheelSize = parseFloat(document.getElementById('wheelSize').value) || 0;
    const tireWidth = parseFloat(document.getElementById('tireWidth').value) || 0;
    const bikeType = document.getElementById('bikeType').value;

    const totalWeight = bikeWeight + riderWeight;
    const wheelDiameter = wheelSize + 2 * tireWidth; // mm
    const wheelCircumference = Math.PI * wheelDiameter / 1000; // metry

    // Średnie ważone parametry trasy
    let totalDistance = 0, weightedSlope = 0, terrainFactor = 0;
    rows.forEach(row => {
      const d = parseFloat(row.dataset.distance);
      const slope = parseFloat(row.dataset.slope);
      const terr = row.dataset.terrain;
      totalDistance += d;
      weightedSlope += d * slope;
      if (terr === 'flat') terrainFactor += d * 1.0;
      if (terr === 'hilly') terrainFactor += d * 1.2;
      if (terr === 'steep') terrainFactor += d * 1.5;
    });

    const avgSlope = weightedSlope / totalDistance;
    const avgTerrainFactor = terrainFactor / totalDistance;

    // prosty model przełożenia
    let basePreference = 2.7;
    basePreference /= (1 + avgSlope/10);
    basePreference /= avgTerrainFactor;

    // korekta wagą
    if (totalWeight > 100) basePreference *= 0.9;
    else if (totalWeight < 70) basePreference *= 1.05;

    // korekta typem roweru
    if (bikeType === 'road') basePreference *= 1.05;
    if (bikeType === 'urban') basePreference *= 0.95;

    // uwzględnienie obwodu koła – lekkie skalowanie
    basePreference *= (wheelCircumference / 2.1); // normalizacja względem ~2.1 m (700x28)

    let rearTeeth = Math.round(frontTeeth / basePreference);
    rearTeeth = Math.max(11, Math.min(40, rearTeeth));

    const suggestions = [11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,30,32,34,36,38,40];
    let nearest = suggestions.reduce((a,b) => Math.abs(b - rearTeeth) < Math.abs(a - rearTeeth) ? b : a);

    document.getElementById('result').innerText = `Optymalna tylna zębatka: ${rearTeeth} — sugerowany wolnobieg: ${nearest}`;
    document.getElementById('details').style.display = 'block';
    document.getElementById('details').innerText = `Średnie nachylenie: ${avgSlope.toFixed(1)}%\nŚredni współczynnik terenu: ${avgTerrainFactor.toFixed(2)}\nObwód koła: ${wheelCircumference.toFixed(2)} m\nWaga całkowita: ${totalWeight.toFixed(1)} kg`;
  }

  // Eventy
  document.getElementById('addSegmentBtn').addEventListener('click', () => addSegment());
  document.getElementById('loadDefaultSegments').addEventListener('click', () => loadDefaultSegments());
  document.getElementById('clearSegments').addEventListener('click', () => clearSegments());
  document.getElementById('calcBtn').addEventListener('click', () => calculateGear());

  // Na start przykładowa trasa
  loadDefaultSegments();
</script>

Kod po stronie serwera

Informacja

Aplikacja nie korzysta z kodu po stronie serwera.

Tagi

JavaScript

Dziękujemy!
()

Informacja o cookies

Moja strona internetowa wykorzystuje wyłącznie niezbędne pliki cookies, które są wymagane do jej prawidłowego działania. Nie używam ciasteczek w celach marketingowych ani analitycznych. Korzystając z mojej strony, wyrażasz zgodę na stosowanie tych plików. Możesz dowiedzieć się więcej w mojej polityce prywatności.