PageSourceSearch

https://caiadocuida.com/descubra-seu-pneu/js/pneu-veiculo.js?v=2026100605

js caiadocuida.com collected 2026-10-09 22:41:42 UTC 48,508 bytes, 1,133 lines download raw bytes

1/* pneu-veiculo, motor de identificacao da medida do pneu pelo modelo do veiculo.
2 *
3 * Sem marca, sem tema, sem texto de campanha e sem nenhum canal de contato:
4 * o motor devolve dados e o projeto que o usa decide o que fazer com eles.
5 *
6 * Funciona no navegador (variavel global PneuVeiculo) e no Node (module.exports).
7 * Nao depende de nada: nem framework, nem biblioteca, nem requisicao a servidor.
8 *
9 * Tabelas normativas embutidas:
10 *   indice de carga (ISO 4209-1 / ETRTO), 0 a 199, em quilos por pneu
11 *   simbolo de velocidade (ISO 4209-1 / ETRTO), de A1 a Y, em km/h
12 *
13 * Dados: os arquivos de dados/ sao gerados por tools/montar.py a partir de
14 * tools/base.tsv, onde cada linha guarda a URL de onde a medida foi lida, e por
15 * tools/pirelli.py, que acrescenta a camada do catalogo de aplicacao da Pirelli
16 * (entradas marcadas com "c":1, cada versao com os proprios anos em "y").
17 * As duas camadas nunca se misturam: a busca mostra a ficha tecnica quando ela existe
18 * e usa o catalogo quando a base nao tem ficha daquele modelo naquele ano.
19 *
20 * 1.3.0 (05/10/2026), base v2 (tools/montar2.py): cada linha traz os proprios anos-modelo em "y" e a
21 * confianca em "q" ('a' alta, 'm' media, 'b' baixa). A linha so aparece nos anos em que vale; ano sem
22 * linha nao cai mais para "todos os anos"; a versao escolhida casa pelo nome exato.
23 */
24(function (raiz, fabrica) {
25  'use strict';
26  var api = fabrica();
27  if (typeof module === 'object' && module.exports) module.exports = api;
28  else raiz.PneuVeiculo = api;
29}(typeof self !== 'undefined' ? self : this, function () {
30  'use strict';
31
32  var VERSAO = '1.3.0';
33
34  // ================================================================ [1] tabelas
35  // Indice de carga -> quilos por pneu. ISO 4209-1, mesma tabela da ETRTO.
36  var INDICE_CARGA = (function () {
37    var baixos = [45, 46.2, 47.5, 48.7, 50, 51.5, 53, 54.5, 56, 58,
38                  60, 61.5, 63, 65, 67, 69, 71, 73, 75, 77.5,
39                  80, 82.5, 85, 87.5, 90, 92.5, 95, 97.5, 100, 103,
40                  106, 109, 112, 115, 118, 121, 125, 128, 132, 136,
41                  140, 145, 150, 155, 160, 165, 170, 175, 180, 185,
42                  190, 195, 200, 206, 212, 218, 224, 230, 236, 243,
43                  250, 257, 265, 272, 280, 290, 300, 307, 315, 325,
44                  335, 345, 355];
45    var altos = [365, 375, 387, 400, 412, 425, 437, 450, 462, 475, 487, 500, 515, 530, 545, 560, 580, 600, 615, 630,
46                 650, 670, 690, 710, 730, 750, 775,
47                 800, 825, 850, 875, 900, 925, 950, 975, 1000, 1030, 1060, 1090, 1120, 1150, 1180, 1215, 1250, 1285,
48                 1320, 1360,
49                 1400, 1450, 1500, 1550, 1600, 1650, 1700, 1750, 1800, 1850, 1900, 1950, 2000, 2060, 2120, 2180, 2240,
50                 2300, 2360, 2430,
51                 2500, 2575, 2650, 2725, 2800, 2900, 3000, 3075, 3150, 3250, 3350, 3450, 3550, 3650, 3750, 3875, 4000,
52                 4125, 4250, 4375,
53                 4500, 4625, 4750, 4875, 5000, 5150, 5300, 5450, 5600, 5800, 6000, 6150, 6300, 6500, 6700, 6900, 7100,
54                 7300, 7500, 7750,
55                 8000, 8250, 8500, 8750, 9000, 9250, 9500, 9750, 10000, 10300, 10600, 10900, 11200, 11500, 11800,
56                 12150, 12500, 12800, 13200, 13600];
57    var t = {}, i;
58    for (i = 0; i < baixos.length; i++) t[i] = baixos[i];          // 0 = 45 kg ... 72 = 355 kg
59    for (i = 0; i < altos.length; i++) t[73 + i] = altos[i];       // 73 = 365 kg ... 199 = 13600 kg
60    return t;
61  })();
62
63  // Simbolo de velocidade -> km/h. ISO 4209-1 / ETRTO.
64  var SIMBOLO_VELOCIDADE = {
65    A1: 5, A2: 10, A3: 15, A4: 20, A5: 25, A6: 30, A7: 35, A8: 40,
66    B: 50, C: 60, D: 65, E: 70, F: 80, G: 90, J: 100, K: 110, L: 120, M: 130,
67    N: 140, P: 150, Q: 160, R: 170, S: 180, T: 190, U: 200, H: 210,
68    V: 240, W: 270, Y: 300, ZR: 240
69  };
70
71  // Serie assumida quando a medida nao traz a serie (padrao historico).
72  var SERIE_PADRAO = 82;
73
74  // ================================================================ [2] utilidades
75  function texto(v) { return v == null ? '' : String(v); }
76
77  function limpa(v) { return texto(v).replace(/\s+/g, ' ').trim(); }
78
79  function semAcento(v) {
80    return limpa(v)
81      .replace(/[áàâãä]/g, 'a').replace(/[ÁÀÂÃÄ]/g, 'A')
82      .replace(/[éèêë]/g, 'e').replace(/[ÉÈÊË]/g, 'E')
83      .replace(/[íìîï]/g, 'i').replace(/[ÍÌÎÏ]/g, 'I')
84      .replace(/[óòôõö]/g, 'o').replace(/[ÓÒÔÕÖ]/g, 'O')
85      .replace(/[úùûü]/g, 'u').replace(/[ÚÙÛÜ]/g, 'U')
86      .replace(/[ç]/g, 'c').replace(/[Ç]/g, 'C')
87      .replace(/[ñ]/g, 'n').replace(/[Ñ]/g, 'N');
88  }
89
90  function chave(v) { return semAcento(v).toLowerCase(); }
91
92  function slug(v) {
93    return chave(v).replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'x';
94  }
95
96  function num(v, padrao) {
97    var n;
98    if (typeof v === 'number') n = v;
99    else {
100      var s = limpa(v);
101      if (s.indexOf(',') >= 0) s = s.replace(/\./g, '').replace(',', '.');
102      n = parseFloat(s);
103    }
104    return isFinite(n) ? n : (padrao === undefined ? 0 : padrao);
105  }
106
107  function arred(n, casas) {
108    var f = Math.pow(10, casas || 0);
109    return Math.round(n * f) / f;
110  }
111
112  function unico(lista) {
113    var vistos = {}, saida = [];
114    for (var i = 0; i < lista.length; i++) {
115      var k = chave(lista[i]);
116      if (!vistos[k]) { vistos[k] = 1; saida.push(lista[i]); }
117    }
118    return saida;
119  }
120
121  // ================================================================ [3] leitura de medida
122  // Formas aceitas, todas testadas em test/formatos.js:
123  //   175/70R14   175/70 R14   175/70r14   175/70-14   175/70/14   17570R14
124  //   P205/55R16  LT265/70R17  225/75R16C  175/70ZR14  185R14 (serie assumid
124a)
125  //   5.60-15     31x10.50R15  175 70 14
126  var RE_METRICA = /(?:P|LT|ST|T)?\s*(\d{3})\s*[\/\s-]\s*(\d{2})\s*(ZR|R|B|D|-|\/)?\s*(\d{2}(?:\.\d)?)\s*(C|LT|CP)?/i;
127  var RE_SEM_SERIE = /(?:P|LT)?\s*(\d{3})\s*(ZR|R|-)\s*(\d{2})\s*(C|LT)?/i;
128  var RE_COLADA = /(\d{3})(\d{2})\s*(ZR|R|-)?\s*(\d{2})\s*(C|LT)?/i;
129  var RE_POLEGADA = /(\d{1,2}(?:[.,]\d{1,2})?)\s*(L)?\s*(ZR|R|-)\s*(\d{2}(?:\.\d)?)\s*(C|LT)?/i;
130  var RE_FLUTUACAO = /(\d{2}(?:[.,]\d{1,2})?)\s*[xX]\s*(\d{1,2}(?:[.,]\d{1,2})?)\s*(ZR|R|-)?\s*(\d{2}(?:\.\d)?)/;
131  // gente escreve "aro 15, 185/60" com o aro separado da medida
132  var RE_LARG_SERIE = /(\d{3})\s*\/\s*(\d{2})(?!\s*[\/\d])/;
133  var RE_ARO_SOLTO = /\b(?:ARO|RODA)\s*(\d{2})\b/i;
134
135  // um numero colado antes do trecho encontrado quase sempre quer dizer que pegamos
136  // o pedaco errado da frase, entao esse casamento nao vale
137  function coladoEmNumero(alvo, m) {
138    if (!m || m.index === 0) return false;
139    return /[\d.,]/.test(alvo.charAt(m.index - 1));
140  }
141
142  // "-" e "D" marcam diagonal de verdade. Sem letra nenhuma (ou com "/" no lugar dela)
143  // quem digitou quase sempre quis dizer radial, entao assumimos radial e marcamos.
144  function construcaoDe(c) {
145    var s = texto(c).toUpperCase();
146    if (s === 'R' || s === 'ZR') return 'radial';
147    if (s === 'B') return 'cinta diagonal';
148    if (s === '-' || s === 'D') return 'diagonal';
149    return 'radial';
150  }
151
152  function construcaoAssumida(c) {
153    var s = texto(c).toUpperCase();
154    return s === '' || s === '/';
155  }
156
157  function lerMedida(cru) {
158    var s = limpa(cru).toUpperCase().replace(/["”]/g, '');
159    if (!s) return null;
160    var m, r = null, alvo = s, achado = null;
161
162    m = RE_FLUTUACAO.exec(s);
163    if (m && !coladoEmNumero(s, m)) {
164      var diamPol = num(m[1].replace(',', '.'));
165      var largPol = num(m[2].replace(',', '.'));
166      r = {
167        tipo: 'flutuacao', larguraPol: largPol, larguraMm: arred(largPol * 25.4, 1),
168        serie: null, construcao: construcaoDe(m[3] || 'R'), aro: num(m[4]),
169        diametroMm: arred(diamPol * 25.4, 1), alturaMm: arred((diamPol * 25.4 - num(m[4]) * 25.4) / 2, 1),
170        sufixo: '', texto: s
171      };
172      achado = m;
173    }
174    if (!r) {
175      m = RE_METRICA.exec(s);
176      if (m && !coladoEmNumero(s, m)) {
177        r = montaMetrica(num(m[1]), num(m[2]), m[3], num(m[4]), m[5], s, false);
178        achado = m;
179      }
180    }
181    if (!r) {
182      m = RE_SEM_SERIE.exec(s);
183      if (m && !coladoEmNumero(s, m)) {
184        r = montaMetrica(num(m[1]), SERIE_PADRAO, m[2], num(m[3]), m[4], s, true);
185        achado = m;
186      }
187    }
188    if (!r) {
189      m = RE_POLEGADA.exec(s);
190      if (m && s.indexOf('/') < 0 && !coladoEmNumero(s, m)) {
191        var lp = num(m[1].replace(',', '.'));
192        var aroP = num(m[4]);
193        var serieP = m[2] ? 60 : 92;   // as flutuacao "L" sao mais baixas que as de carga
194        var altP = lp * 25.4 * serieP / 100;
195        r = {
196          tipo: 'polegada', larguraPol: lp, larguraMm: arred(lp * 25.4, 1),
197          serie: null, serieAssumida: serieP, construcao: construcaoDe(m[3]), aro: aroP,
198          alturaMm: arred(altP, 1), diametroMm: arred(aroP * 25.4 + 2 * altP, 1),
199          sufixo: (m[5] || '').toUpperCase(), texto: s
200        };
201        achado = m;
202      }
203    }
204    if (!r) {
205      alvo = s.replace(/\s+/g, '');
206      m = RE_COLADA.exec(alvo);
207      if (m && !coladoEmNumero(alvo, m)) {
208        r = montaMetrica(num(m[1]), num(m[2]), m[3], num(m[4]), m[5], s, false);
209        achado = m;
210      }
211    }
212    if (!r) {
213      alvo = s;
214      var ls = RE_LARG_SERIE.exec(s), ar = RE_ARO_SOLTO.exec(s);
215      if (ls && ar) {
216        r = montaMetrica(num(ls[1]), num(ls[2]), '', num(ar[1]), '', s, false);
217        r.aroSolto = true;
218        achado = ls;
219      }
220    }
221    if (!r) return null;
222    if (r.aro < 8 || r.aro > 34) return null;
223    // o indice fica fora do trecho da medida, senao "225/75R16C" viraria indice 16 simbolo C
224    r.bruto = achado[0];
225    var antes = alvo.slice(0, achado.index);
226    var depois = alvo.slice(achado.index + achado[0].length);
227    var idx = lerIndice(depois) || lerIndice(antes);
228    if (idx) r.indice = idx;
229    if (/\bXL\b|REINFOR|EXTRA ?LOAD/.test(s)) r.reforcado = true;
230    r.normal = normalizarMedida(r);
231    r.circunferenciaMm = arred(Math.PI * r.diametroMm, 1);
232    r.voltasPorKm = r.circunferenciaMm ? arred(1000000 / r.circunferenciaMm, 1) : 0;
233    return r;
234  }
235
236  function montaMetrica(largura, serie, constr, aro, sufixo, s, assumida) {
237    var altura = largura * serie / 100;
238    var o = {
239      tipo: 'metrica', larguraMm: largura, serie: serie, construcao: construcaoDe(constr),
240      aro: aro, alturaMm: arred(altura, 1), diametroMm: arred(aro * 25.4 + 2 * altura, 1),
241      sufixo: (sufixo || '').toUpperCase(), texto: s
242    };
243    if (construcaoAssumida(constr)) o.construcaoAssumida = true;
244    if (assumida) { o.serieAssumida = serie; o.serie = null; }
245    if (String(constr || '').toUpperCase() === 'ZR') o.zr = true;
246    return o;
247  }
248
249  function normalizarMedida(r) {
250    if (!r) return '';
251    if (r.tipo === 'flutuacao') {
252      return arred(r.diametroMm / 25.4, 2) + 'x' + arred(r.larguraPol, 2) +
253        (r.construcao === 'radial' ? 'R' : '-') + r.aro;
254    }
255    if (r.tipo === 'polegada') {
256      return arred(r.larguraPol, 2) + (r.construcao === 'radial' ? 'R' : '-') + r.aro + (r.sufixo || '');
257    }
258    var serie = r.serie == null ? '' : ('/' + (r.serie < 10 ? '0' + r.serie : r.serie));
259    return r.larguraMm + serie + (r.construcao === 'radial' ? 'R' : '-') + r.aro + (r.sufixo || '');
260  }
261
262  function medidaValida(cru) { return !!lerMedida(cru); }
263
264  // ================================================================ [4] leitura de indice
265  var RE_INDICE = /(\d{2,3})\s*(?:\/\s*(\d{2,3}))?\s*\(?\s*(A[1-8]|ZR|[B-Z])\s*\)?(?!\d)/i;
266
267  function lerIndice(cru) {
268    var s = limpa(cru).toUpperCase();
269    if (!s) return null;
270    var m = RE_INDICE.exec(s);
271    if (!m) return null;
272    var li = parseInt(m[1], 10);
273    var liPar = m[2] ? parseInt(m[2], 10) : null;
274    var sim = m[3].toUpperCase();
275    if (!(li in INDICE_CARGA)) return null;
276    if (!(sim in SIMBOLO_VELOCIDADE)) return null;
277    var o = {
278      indice: li, cargaKg: INDICE_CARGA[li], simbolo: sim, velocidadeKmh: SIMBOLO_VELOCIDADE[sim],
279      texto: (m[2] ? li + '/' + liPar : String(li)) + sim
280    };
281    if (liPar != null) {
282      o.indicePar = liPar;
283      o.cargaParKg = INDICE_CARGA[liPar] || null;
284      o.rodadoDuplo = true;
285    }
286    return o;
287  }
288
289  function cargaDoIndice(i) { return INDICE_CARGA[parseInt(i, 10)] || null; }
290
291  function velocidadeDoSimbolo(s) {
292    return SIMBOLO_VELOCIDADE[limpa(s).toUpperCase()] || null;
293  }
294
295  function indiceParaCarga(kg) {
296    var alvo = num(kg), achou = null;
297    for (var i in INDICE_CARGA) {
298      if (INDICE_CARGA[i] >= alvo && (achou === null || INDICE_CARGA[i] < INDICE_CARGA[achou])) achou = i;
299    }
300    return achou === null ? null : { indice: parseInt(achou, 10), cargaKg: INDICE_CARGA[achou] };
301  }
302
303  // ================================================================ [5] comparacao de medidas
304  function comparar(a, b) {
305    var x = lerMedida(a), y = lerMedida(b);
306    if (!x || !y) return null;
307    var difMm = arred(y.diametroMm - x.diametroMm, 1);
308    var pct = x.diametroMm ? arred(100 * difMm / x.diametroMm, 2) : 0;
309    return {
310      de: x.normal, para: y.normal,
311      diametroDe: x.diametroMm, diametroPara: y.diametroMm,
312      diferencaMm: difMm, diferencaPct: pct,
313      diferencaAlturaMm: arred(y.alturaMm - x.alturaMm, 1),
314      diferencaLarguraMm: arred((y.larguraMm || 0) - (x.larguraMm || 0), 1),
315      mesmoAro: x.aro === y.aro,
316      // 3% e o limite usual de tolerancia de diametro em troca de medida
317      dentroDe3Pct: Math.abs(pct) <= 3,
318      erroVelocimetroPct: arred(-pct, 2)
319    };
320  }
321
322  function equivalentes(medida, toleranciaPct, universo) {
323    var base = lerMedida(medida);
324    if (!base) return [];
325    var tol = toleranciaPct == null ? 3 : num(toleranciaPct, 3);
326    var lista = universo || medidasConhecidas();
327    var saida = [];
328    for (var i = 0; i < lista.length; i++) {
329      var c = comparar(base.normal, lista[i]);
330      if (!c || c.para === base.normal) continue;
331      if (Math.abs(c.diferencaPct) <= tol) {
332        saida.push({ medida: c.para, diferencaMm: c.diferencaMm, diferencaPct: c.diferencaPct,
333                     mesmoAro: c.mesmoAro, veiculos: (MEDIDAS[c.para] || 0) });
334      }
335    }
336    saida.sort(function (p, q) { return Math.abs(p.diferencaPct) - Math.abs(q.diferencaPct); });
337    return saida;
338  }
339
340  // ================================================================ [6] dados
341  var MARCAS = {};      // slug -> nome
342  var VEICULOS = {};    // slug -> [ {n, a1, a2, vs:[...]} ]
343  var FONTES = [];      // [codigo, url]
344  var MEDIDAS = {};     // medida -> quantos veiculos usam
345  var CONFIG = { caminho: 'dados/', tempoLimite: 12000, catalogo: true, versao: '' };
346  var NOME_FONTE = {
347    of: 'ficha ou catalogo oficial da montadora',
348    ca: 'ficha oficial da montadora hospedada por terceiro',
349    eo: 'fabricante de pneu declarando o equipamento original',
350    ae: 'imprensa especializada',
351    ga: 'imprensa especializada',
352    im: 'imprensa especializada ou base do setor de transporte',
353    fc: 'base de fichas tecnicas',
354    au: 'base de fichas tecnicas',
355    gp: 'guia de aplicacao de fabricante de pneu',
356    pc: 'catalogo de aplicacao da Pirelli',
357    ou: 'outra fonte'
358  };
359  // camada separada: o que uma loja associa ao veiculo. NAO e medida de fabrica.
360  var COMPAT = {};      // slug da marca -> {nome, vs:[[veiculo da loja, medida, indice]]}
361  var INDICE_LOJA = {}; // medida -> indices que a loja vende
362
363  function configurar(o) {
364    o = o || {};
365    if (o.caminho != null) CONFIG.caminho = texto(o.caminho);
366    if (o.tempoLimite != null) CONFIG.tempoLimite = num(o.tempoLimite, 12000);
367    // catalogo:false deixa o motor so com a base de ficha tecnica
368    if (o.catalogo != null) CONFIG.catalogo = !!o.catalogo;
369    // versao dos dados: vai como ?v= no endereco do arquivo da marca, para o navegador nao usar copia velha
370    if (o.versao != null) CONFIG.versao = texto(o.versao);
371    return CONFIG;
372  }
373
374  /** Entradas de uma marca. As do catalogo ("c":1) somem com configurar({catalogo:false}). */
375  function entradas(s) {
376    var lista = VEICULOS[s] || [];
377    if (CONFIG.catalogo) return lista;
378    var saida = [];
379    for (var i = 0; i < lista.length; i++) if (!lista[i].c) saida.push(lista[i]);
380    return saida;
381  }
382
383  /** A linha vale para o ano? Linha do catalogo traz os proprios anos em "y". */
384  function valeNoAno(v, g, a) {
385    if (!a) return true;
386    if (a < g.a1 || a > g.a2) return false;
387    return !v.y || (a >= v.y[0] && a <= v.y[1]);
388  }
389
390  /** Injeta dados na mao. Serve para Node, para teste e para quem embute tudo num arquivo. */
391  function usarDados(d) {
392    d = d || {};
393    if (d.marcas) MARCAS = d.marcas;
394    if (d.veiculos) for (var s in d.veiculos) VEICULOS[s] = d.veiculos[s];
395    if (d.fontes) FONTES = d.fontes;
396    if (d.medidas) MEDIDAS = d.medidas;
397    else recontarMedidas();
398    if (d.compat) COMPAT = d.compat;
399    if (d.indiceLoja) INDICE_LOJA = d.indiceLoja;
400    return { marcas: contaChaves(MARCAS), veiculos: contaChaves(VEICULOS), fontes: FONTES.length,
401             compat: contaChaves(COMPAT) };
402  }
403
404  function contaChaves(o) { var n = 0; for (var k in o) if (o.hasOwnProperty(k)) n++; return n; }
405
406  function recontarMedidas() {
407    MEDIDAS = {};
408    for (var s in VEICULOS) {
409      var lista = entradas(s);
410      for (var i = 0; i < lista.length; i++) {
411        var vs = lista[i].vs || [];
412        for (var j = 0; j < vs.length; j++) MEDIDAS[vs[j].m] = (MEDIDAS[vs[j].m] || 0) + 1;
413      }
414    }
415  }
416
417  function pegaGlobais() {
418    var g = (typeof window !== 'undefined') ? window : null;
419    if (!g) return;
420    if (g.PV_MARCAS) MARCAS = g.PV_MARCAS;
421    if (g.PV_FONTES) FONTES = g.PV_FONTES;
422    if (g.PV_MEDIDAS) MEDIDAS = g.PV_MEDIDAS;
423    if (g.PV_VEICULOS) for (var s in g.PV_VEICULOS) VEICULOS[s] = g.PV_VEICULOS[s];
424    if (g.PV_COMPAT) COMPAT = g.PV_COMPAT;
425    if (g.PV_INDICE_LOJA) INDICE_LOJA = g.PV_INDICE_LOJA;
426  }
427
428  /** Carrega o arquivo de uma marca. No navegador injeta <script>; no Node le do disco. */
429  function carregar(marca, pronto, erro) {
430    var s = slugDaMarca(marca);
431    pegaGlobais();
432    if (!s) { if (erro) erro(new Error('marca desconhecida: ' + marca)); return; }
433    if (VEICULOS[s]) { if (pronto) pronto(VEICULOS[s]); return; }
434    if (typeof window === 'undefined' || !window.document) {
435      try {
436        var fs = require('fs'), caminho = require('path').join(CONFIG.caminho, 'veiculos', s + '.js');
437        var codigo = fs.readFileSync(caminho, 'utf8');
438        var alvo = { PV_VEICULOS: {} };
439        (new Function('window', codigo))(alvo);
440        VEICULOS[s] = alvo.PV_VEICULOS[s];
441        recontarMedidas();
442        if (pronto) pronto(VEICULOS[s]);
443      } catch (e) { if (erro) erro(e); }
444      return;
445    }
446    var el = window.document.createElement('script');
447    var caiu = false;
448    el.src = CONFIG.caminho + 'veiculos/' + s + '.js' + (CONFIG.versao ? '?v=' + encodeURIComponent(CONFIG.versao) : '');
449    el.async = true;
450    el.onload = function () {
451      pegaGlobais(); recontarMedidas();
452      if (VEICULOS[s]) { if (pronto) pronto(VEICULOS[s]); }
453      else if (erro) erro(new Error('arquivo carregado sem dados: ' + s));
454    };
455    el.onerror = function () { caiu = true; if (erro) erro(new Error('falhou ao carregar ' + el.src)); };
456    window.setTimeout(function () {
457      if (!caiu && !VEICULOS[s] && erro) erro(new Error('tempo esgotado ao carregar ' + el.src));
458    }, CONFIG.tempoLimite);
459    window.document.head.appendChild(el);
460  }
461
462  function carregarTudo(pronto, erro) {
463    pegaGlobais();
464    var lista = marcas(), i = 0, faltam = lista.length;
465    if (!faltam) { if (pronto) pronto(); return; }
466    for (i = 0; i < lista.length; i++) {
467      carregar(lista[i].slug, function () { if (--faltam === 0 && pronto) pronto(); },
468               function (e) { if (--faltam === 0 && pronto) pronto(); if (erro) erro(e); });
469    }
470  }
471
472  // ================================================================ [7] consulta
473  function marcas() {
474    pegaGlobais();
475    var saida = [];
476    for (var s in MARCAS) if (MARCAS.hasOwnProperty(s)) saida.push({ slug: s, nome: MARCAS[s] });
477    saida.sort(function (a, b) { return chave(a.nome) < chave(b.nome) ? -1 : 1; });
478    return saida;
479  }
480
481  function slugDaMarca(marca) {
482    pegaGlobais();
483    var k = chave(marca);
484    if (!k) return '';
485    if (MARCAS[k]) return k;
486    var alvo = slug(marca);
487    if (MARCAS[alvo]) return alvo;
488    for (var s in MARCAS) {
489      if (chave(MARCAS[s]) === k || s === alvo) return s;
490    }
491    for (var s2 in MARCAS) {
492      if (chave(MARCAS[s2]).indexOf(k) === 0) return s2;
493    }
494    return '';
495  }
496
497  function listaDaMarca(marca) {
498    var s = slugDaMarca(marca);
499    if (!s) return [];
500    pegaGlobais();
501    return entradas(s);
502  }
503
504  /** Modelos de uma marca, com as geracoes de cada um. */
505  function modelos(marca) {
506    var lista = listaDaMarca(marca), por = {}, ordem = [];
507    for (var i = 0; i < lista.length; i++) {
508      var k = chave(lista[i].n);
509      if (!por[k]) { por[k] = { nome: lista[i].n, geracoes: [], a1: lista[i].a1, a2: lista[i].a2 }; ordem.push(k); }
510      por[k].geracoes.push({ a1: lista[i].a1, a2: lista[i].a2, versoes: (lista[i].vs || []).length,
511                             catalogo: !!lista[i].c });
512      por[k].a1 = Math.min(por[k].a1, lista[i].a1);
513      por[k].a2 = Math.max(por[k].a2, lista[i].a2);
514    }
515    var saida = [];
516    for (var j = 0; j < ordem.length; j++) saida.push(por[ordem[j]]);
517    saida.sort(function (a, b) { return chave(a.nome) < chave(b.nome) ? -1 : 1; });
518    return saida;
519  }
520
521  function geracoes(marca, modelo) {
522    var lista = listaDaMarca(marca), k = chave(modelo), saida = [];
523    for (var i = 0; i < lista.length; i++) if (chave(lista[i].n) === k) saida.push(lista[i]);
524    saida.sort(function (a, b) { return b.a1 - a.a1; });
525    return saida;
526  }
527
528  function anos(marca, modelo) {
529    var gs = geracoes(marca, modelo), vistos = {}, saida = [];
530    for (var i = 0; i < gs.length; i++) {
531      var vs = gs[i].vs || [];
532      for (var j = 0; j < vs.length; j++) {
533        var de = vs[j].y ? Math.max(vs[j].y[0], gs[i].a1) : gs[i].a1;
534        var ate = vs[j].y ? Math.min(vs[j].y[1], gs[i].a2) : gs[i].a2;
535        for (var a = ate; a >= de; a--) if (!vistos[a]) { vistos[a] = 1; saida.push(a); }
536      }
537    }
538    saida.sort(function (a, b) { return b - a; });
539    return saida;
540  }
541
542  function fonteDe(v) {
543    var ids = v.s || [], saida = [];
544    for (var i = 0; i < ids.length; i++) {
545      var f = FONTES[ids[i]];
546      if (f) saida.push({ codigo: f[0], tipo: NOME_FONTE[f[0]] || 'outra fonte', url: f[1] });
547    }
548    return saida;
549  }
550
551  function enfeita(v, modelo, marcaNome, g) {
552    var med = lerMedida(v.m) || {};
553    var idx = v.i ? lerIndice(v.i) : null;
554    return {
555      marca: marcaNome, modelo: modelo, a1: v.y ? v.y[0] : g.a1, a2: v.y ? v.y[1] : g.a2,
556      versao: v.v, medida: v.m,
557      // 'ficha': base de ficha tecnica; 'catalogo': catalogo de aplicacao da Pirelli
558      camada: g.c ? 'catalogo' : 'ficha',
559      indice: v.i || '', cargaKg: idx ? idx.cargaKg : null,
560      velocidadeKmh: idx ? idx.velocidadeKmh : null,
561      eixo: v.e === 'd' ? 'dianteiro' : (v.e === 't' ? 'traseiro' : 'todos'),
562      aro: med.aro || null, larguraMm: med.larguraMm || null, serie: med.serie,
563      diametroMm: med.diametroMm || null,
564      anoDaFonte: v.y ? v.y[0] : (v.ar || null), anoDaFonteAte: v.y ? v.y[1] : (v.ar2 || null),
565      qualidadeDaFonte: v.f || '', tipoDaFonte: NOME_FONTE[v.f] || '',
566      // de onde veio o indice: ficha da montadora, fabricante de pneu declarando o original, ou
567      // guia de aplicacao (que recomenda, mas nao declara o original)
568      fonteDoIndice: v.i ? (v.ic || v.f || '') : '',
569      indiceDoGuia: !!(v.i && v.ic === 'gp'),
570      fontes: fonteDe(v), observacao: v.o || '',
571      // 'alta', 'media' ou 'baixa'; linha sem "q" (base antiga) conta como media
572      confianca: v.q === 'a' ? 'alta' : (v.q === 'b' ? 'baixa' : 'media')
573    };
574  }
575
576  /** Versoes de um modelo num ano, das duas camadas (a da ficha tecnica vem primeiro).
577   *  Sem ano, devolve as versoes de todas as geracoes. */
578  function versoes(marca, modelo, ano) {
579    var gs = geracoes(marca, modelo), a = ano ? num(ano) : 0, saida = [];
580    var nome = MARCAS[slugDaMarca(marca)] || limpa(marca);
581    for (var i = 0; i < gs.length; i++) {
582      if (a && (a < gs[i].a1 || a > gs[i].a2)) continue;
583      var vs = gs[i].vs || [];
584      for (var j = 0; j < vs.length; j++) {
585        if (valeNoAno(vs[j], gs[i], a)) saida.push(enfeita(vs[j], gs[i].n, nome, gs[i]));
586      }
587    }
588    saida.sort(function (p, q) {
589      if (p.camada !== q.camada) return p.camada === 'ficha' ? -1 : 1;
590      var kp = chave(p.versao), kq = chave(q.versao);
591      if (kp !== kq) return kp.localeCompare(kq, 'pt-BR', { numeric: true });
592      if (p.eixo !== q.eixo) return p.eixo === 'dianteiro' ? -1 : (q.eixo === 'dianteiro' ? 1 : 0);
593      if (p.aro !== q.aro) return (p.aro || 0) - (q.aro || 0);
594      return p.medida < q.medida ? -1 : (p.medida > q.medida ? 1 : 0);
595    });
596    return saida;
597  }
598
599  /** Separa a lista nas duas camadas. */
600  function porCamada(lista) {
601    var r = { ficha: [], catalogo: [] };
602    for (var i = 0; i < lista.length; i++) r[lista[i].camada === 'catalogo' ? 'catalogo' : 'ficha'].push(lista[i]);
603    return r;
604  }
605
606  function filtraVersao(lista, versao) {
607    var k = chave(versao);
608    // a opcao escolhida na lista e o nome exato da versao: so ela, sem pegar "LT 1.0 Turbo" junto de "LT 1.0"
609    var exatas = lista.filter(function (x) { return chave(x.versao) === k; });
610    if (exatas.length) return exatas;
611    var palavras = k.split(/\s+/).filter(function (x) { return x.length > 0; });
612    var filtrado = [];
613    for (var i = 0; i < lista.length; i++) {
614      var rotulo = chave(lista[i].versao);
615      // casa se o rotulo contem o texto, se o texto contem o rotulo, ou se toda palavra
616      // do texto aparece no rotulo (e assim "ltz turbo" acha "Turbo LT / LTZ / Premier RS")
617      var todas = palavras.length > 0 && palavras.every(function (w) { return rotulo.indexOf(w) >= 0; });
618      if (rotulo.indexOf(k) >= 0 || k.indexOf(rotulo) >= 0 || todas) filtrado.push(lista[i]);
619    }
620    return filtrado;
621  }
622
623  function medidasDe(lista) {
624    var saida = [];
625    for (var i = 0; i < lista.length; i++) if (saida.indexOf(lista[i].medida) < 0) saida.push(lista[i].medida);
626    return saida;
627  }
628
629  /** Compara as medidas das duas camadas: 'batem', 'batem em parte' ou 'divergem'. */
630  function conferir(a, b) {
631    function norm(lista) {
632      var s = {};
633      for (var i = 0; i < lista.length; i++) {
634        var m = lerMedida(lista[i]);
635        s[m ? m.normal.replace(/C$/, '') : chave(lista[i])] = 1;
636      }
637      return s;
638    }
639    var x = norm(a), y = norm(b), iguais = 0, nx = contaChaves(x), ny = contaChaves(y);
640    for (var k in x) if (y[k]) iguais++;
641    if (iguais === nx && iguais === ny) return 'batem';
642    return iguais ? 'batem em parte' : 'divergem';
643  }
644
645  /** Busca principal. Devolve as medidas do veiculo e os avisos que o caso merece. */
646  function buscar(p) {
647    p = p || {};
648    var r = { ok: false, marca: '', modelo: '', ano: null, versoes: [], medidas: [],
649              principal: null, avisos: [], fontes: [] };
650    var s = slugDaMarca(p.marca);
651    if (!s) { r.avisos.push('marca não encontrada na base'); return r; }
652    r.marca = MARCAS[s];
653    if (!p.modelo) { r.avisos.push('informe o modelo'); return r; }
654    var achados = geracoes(s, p.modelo);
655    if (!achados.length) {
656      var alt = acharModelo(s, p.modelo);
657      if (alt) { achados = geracoes(s, alt); r.avisos.push('modelo lido como "' + alt + '"'); }
658    }
659    if (!achados.length) { r.avisos.push('modelo não encontrado nesta marca'); return r; }
660    r.modelo = achados[0].n;
661    r.ano = p.ano ? num(p.ano) : null;
662    var lista = versoes(s, r.modelo, r.ano);
663    if (!lista.length && r.ano) {
664      // antes caia para "todos os anos" e mandava medida de outro ano; agora nao responde
665      r.avisos.push('a base não tem esse ano para esse modelo');
666      return r;
667    }
668    // A ficha tecnica manda. O catalogo Pirelli vem junto, em r.catalogo, para conferir; e so vira
669    // a resposta principal quando a base nao tem ficha desse modelo nesse ano.
670    var camadas = porCamada(lista);
671    var principal = camadas.ficha.length ? camadas.ficha : camadas.catalogo;
672    var doCatalogo = camadas.catalogo;
673    r.camada = camadas.ficha.length ? 'ficha' : (camadas.catalogo.length ? 'catalogo' : '');
674    if (p.versao) {
675      var filtrado = filtraVersao(principal, p.versao);
676      if (!filtrado.length && r.camada === 'ficha' && camadas.catalogo.length) {
677        filtrado = filtraVersao(camadas.catalogo, p.versao);
678        if (filtrado.length) {
679          r.camada = 'catalogo';
680          r.avisos.push('versão achada só no catálogo Pirelli');
681        }
682      }
683      if (filtrado.length) principal = filtrado;
684      else r.avisos.push('versão não encontrada; mostrando todas as versões do ano');
685      var catFiltrado = filtraVersao(camadas.catalogo, p.versao);
686      if (catFiltrado.length) doCatalogo = catFiltrado;
687    }
688    lista = principal;
689    r.versoes = lista;
690    r.catalogo = r.camada === 'ficha' ? doCatalogo : [];
691    r.medidasCatalogo = medidasDe(doCatalogo);
692    r.conferencia = '';
693    if (camadas.ficha.length && doCatalogo.length) {
694      r.conferencia = conferir(medidasDe(r.camada === 'ficha' ? lista : camadas.ficha), medidasDe(doCatalogo));
695    }
696    if (r.camada === 'catalogo') {
697      r.avisos.push('a base não tem ficha técnica desse modelo nesse ano; as medidas vêm do catálogo de ' +
698        'aplicação da Pirelli');
699    } else if (r.conferencia === 'divergem') {
700      r.avisos.push('a ficha técnica e o catálogo Pirelli não têm nenhuma medida em comum nesse ano; ' +
701        'confirme pela etiqueta da porta');
702    }
703    var medidas = [], fontes = [];
704    for (var j = 0; j < lista.length; j++) {
705      if (medidas.indexOf(lista[j].medida) < 0) medidas.push(lista[j].medida);
706      for (var f = 0; f < lista[j].fontes.length; f++) {
707        if (fontes.indexOf(lista[j].fontes[f].url) < 0) fontes.push(lista[j].fontes[f].url);
708      }
709      if (lista[j].observacao) r.avisos.push(lista[j].versao + ': ' + lista[j].observacao);
710    }
711    r.medidas = medidas;
712    r.fontes = fontes;
713    // medida que pode ir para o cliente: so das linhas de confianca alta ou media
714    r.medidasConfiaveis = [];
715    r.aConfirmar = false;
716    for (var c = 0; c < lista.length; c++) {
717      if (lista[c].confianca === 'baixa') { r.aConfirmar = true; continue; }
718      if (r.medidasConfiaveis.indexOf(lista[c].medida) < 0) r.medidasConfiaveis.push(lista[c].medida);
719    }
720    if (r.aConfirmar) r.avisos.push('há versão com medida não confirmada nesse ano; a equipe confirma');
721    r.principal = lista.length ? lista[0] : null;
722    r.ok = lista.length > 0;
723    if (medidas.length > 1) {
724      r.avisos.push('esse modelo sai de fábrica com ' + medidas.length +
725        ' medidas diferentes; confirme pela etiqueta da porta ou pelo pneu que está no carro');
726    }
727    var semIndice = 0, doGuia = 0;
728    for (var g = 0; g < lista.length; g++) {
729      if (!lista[g].indice) semIndice++;
730      if (lista[g].indiceDoGuia) doGuia++;
731    }
732    if (semIndice === lista.length && lista.length) {
733      r.avisos.push('a fonte publica só a medida, sem o índice de carga e velocidade');
734    }
735    if (doGuia) {
736      r.avisos.push('o índice de ' + doGuia + ' linha(s) vem do guia de aplicação de um fabricante de pneu, ' +
737        'que recomenda o pneu mas não declara o original; confirme na etiqueta da porta');
738    }
739    return r;
740  }
741
742  /** Distancia de edicao (Levenshtein). Serve para aceitar "corola" no lugar de "Corolla". */
743  function distancia(a, b) {
744    if (a === b) return 0;
745    if (!a.length) return b.length;
746    if (!b.length) return a.length;
747    var linha = [], i, j;
748    for (j = 0; j <= b.length; j++) linha[j] = j;
749    for (i = 1; i <= a.length; i++) {
750      var anterior = linha[0];
751      linha[0] = i;
752      for (j = 1; j <= b.length; j++) {
753        var guarda = linha[j];
754        linha[j] = Math.min(linha[j] + 1, linha[j - 1] + 1,
755                            anterior + (a.charAt(i - 1) === b.charAt(j - 1) ? 0 : 1));
756        anterior = guarda;
757      }
758    }
759    return linha[b.length];
760  }
761
762  function erroAceitavel(a, b) {
763    var maior = Math.max(a.length, b.length);
764    if (maior <= 3) return 0;
765    return maior >= 6 ? 2 : 1;
766  }
767
768  function acharModelo(marca, texto) {
769    var lista = listaDaMarca(marca), k = chave(texto), melhor = '', nota = 0;
770    var soLetras = k.replace(/[^a-z0-9]/g, '');
771    for (var i = 0; i < lista.length; i++) {
772      var n = chave(lista[i].n), p = 0;
773      var nLetras = n.replace(/[^a-z0-9]/g, '');
774      if (n === k) p = 100;
775      else if (nLetras === soLetras) p = 90;
776      else if (n.indexOf(k) === 0 || k.indexOf(n) === 0) p = 80 - Math.min(10, Math.abs(n.length - k.length));
777      else if (n.indexOf(k) >= 0 || k.indexOf(n) >= 0) p = 65 - Math.min(10, Math.abs(n.length - k.length));
778      else {
779        var d = distancia(nLetras, soLetras);
780        if (d <= erroAceitavel(nLetras, soLetras)) p = 60 - 10 * d;
781      }
782      if (p > nota) { nota = p; melhor = lista[i].n; }
783    }
784    return nota >= 40 ? melhor : '';
785  }
786
787  /** Indice reverso: quem sai de fabrica com essa medida. Precisa das marcas carregadas. */
788  function porMedida(medida) {
789    var alvo = lerMedida(medida);
790    if (!alvo) return [];
791    pegaGlobais();
792    var saida = [];
793    for (var s in VEICULOS) {
794      var nome = MARCAS[s] || s, lista = entradas(s);
795      for (var i = 0; i < lista.length; i++) {
796        var m = lista[i], vs = m.vs || [];
797        for (var j = 0; j < vs.length; j++) {
798          var lida = lerMedida(vs[j].m);
799          if (lida && lida.normal === alvo.normal) saida.push(enfeita(vs[j], m.n, nome, m));
800        }
801      }
802    }
803    saida.sort(function (p, q) {
804      if (chave(p.marca) !== chave(q.marca)) return chave(p.marca) < chave(q.marca) ? -1 : 1;
805      if (chave(p.modelo) !== chave(q.modelo)) return chave(p.modelo) < chave(q.modelo) ? -1 : 1;
806      if (p.camada !== q.camada) return p.camada === 'ficha' ? -1 : 1;
807      return q.a1 - p.a1;
808    });
809    return saida;
810  }
811
812  function medidasConhecidas() {
813    pegaGlobais();
814    if (!CONFIG.catalogo || !contaChaves(MEDIDAS)) recontarMedidas();
815    var saida = [];
816    for (var m in MEDIDAS) if (MEDIDAS.hasOwnProperty(m)) saida.push(m);
817    saida.sort();
818    return saida;
819  }
820
821  /** Busca livre por texto, do tipo "gol 2012" ou "hilux srv". */
822  function sugerir(consulta, limite) {
823    pegaGlobais();
824    var termos = chave(consulta).split(/\s+/).filter(function (t) { return t.length > 1; });
825    if (!termos.length) return [];
826    var ano = null;
827    for (var t = 0; t < termos.length; t++) {
828      if (/^(19|20)\d{2}$/.test(termos[t])) { ano = parseInt(termos[t], 10); }
829    }
830    // uma sugestao por modelo: as geracoes e as duas camadas do mesmo modelo se somam
831    var por = {}, ordem = [];
832    for (var s in VEICULOS) {
833      var nome = MARCAS[s] || s, lista = entradas(s);
834      for (var i = 0; i < lista.length; i++) {
835        var m = lista[i];
836        var kModelo = chave(m.n), kMarca = chave(nome);
837        var nota = 0, casou = 0;
838        for (var j = 0; j < termos.length; j++) {
839          if (/^(19|20)\d{2}$/.test(termos[j])) continue;
840          var termo = termos[j], p = 0;
841          // nome exato do modelo vale mais que pedaco de nome, senao "gol" cairia atras de "golf"
842          if (kModelo === termo) p = 10;
843          else if (kModelo.indexOf(termo) === 0) p = 6 - Math.min(3, (kModelo.length - termo.length) * 0.5);
844          else if (kModelo.indexOf(termo) > 0) p = 3;
845          if (kMarca === termo) p = Math.max(p, 7);
846          else if (kMarca.indexOf(termo) === 0) p = Math.max(p, 5);
847          else if (kMarca.indexOf(termo) > 0) p = Math.max(p, 2);
848          if (p > 0) { casou++; nota += p; }
849        }
850        var precisa = termos.length - (ano ? 1 : 0);
851        if (casou < precisa || precisa === 0) continue;
852        if (ano) {
853          if (ano < m.a1 || ano > m.a2) continue;
854          nota += 2;
855        }
856        var km = s + '|' + kModelo, item = por[km];
857        if (!item) {
858          item = por[km] = { marca: nome, marcaSlug: s, modelo: m.n, a1: m.a1, a2: m.a2, versoes: 0,
859                             nota: nota, ano: ano, fichaTecnica: false, catalogo: false };
860          ordem.push(km);
861        }
862        item.a1 = Math.min(item.a1, m.a1);
863        item.a2 = Math.max(item.a2, m.a2);
864        item.versoes += (m.vs || []).length;
865        item.nota = Math.max(item.nota, nota);
866        if (m.c) item.catalogo = true; else item.fichaTecnica = true;
867      }
868    }
869    var saida = [];
870    for (var o = 0; o < ordem.length; o++) saida.push(por[ordem[o]]);
871    saida.sort(function (p, q) { return q.nota - p.nota || q.a2 - p.a2; });
872    return saida.slice(0, limite || 12);
873  }
874
875  // ================================================================ [7b] compatibilidade da loja
876  // Nome da loja: "ONIX PLUS LT SEDAN", "L200 TRITON HPE PICKUP", "X1 SDRIVE 20I (E84) SUV".
877  // Casa com o modelo da base quando comeca pelas mesmas palavras. Se dois modelos da marca casam
878  // ("Onix" e "Onix Plus"), fica com o de nome mais comprido.
879  function palavras(t) { return chave(t).replace(/[^a-z0-9]+/g, ' ').trim().split(/\s+/).filter(
879Boolean); }
880
881  function casaInicio(nomeLoja, modelo) {
882    var a = palavras(nomeLoja), b = palavras(modelo);
883    if (!a.length || !b.length) return 0;
884    var i = 0;
885    for (; i < b.length; i++) if (a[i] !== b[i]) break;
886    if (i === b.length) return b.length;
887    // "HB20" contra "HB 20": junta as palavras da loja ate cobrir o nome do modelo sem espaco
888    var alvo = b.join(''), junto = '';
889    for (var j = 0; j < a.length && junto.length < alvo.length; j++) {
890      junto += a[j];
891      if (junto === alvo) return j + 1;
892    }
893    return 0;
894  }
895
896  /** O que a loja associa a um modelo, agrupado por medida. Nao tem ano e NAO e medida de fabrica. */
897  function compativeis(marca, modelo) {
898    pegaGlobais();
899    var s = slugDaMarca(marca) || slug(marca), bloco = COMPAT[s];
900    if (!bloco) {
901      for (var k in COMPAT) if (chave(COMPAT[k].nome) === chave(marca)) { bloco = COMPAT[k]; break; }
902    }
903    var saida = { marca: bloco ? bloco.nome : limpa(marca), modelo: limpa(modelo), medidas: [],
904                  versoesDaLoja: 0,
905                  aviso: 'lista da loja: inclui medida alternativa e plus size, não diz ano e não é a medida de fábrica' };
906    if (!bloco || !modelo) return saida;
907    var outros = modelos(s).map(function (m) { return m.nome; });
908    var por = {}, ordem = [], vistas = {};
909    for (var i = 0; i < bloco.vs.length; i++) {
910      var nomeLoja = bloco.vs[i][0], med = bloco.vs[i][1], idx = bloco.vs[i][2];
911      var n = casaInicio(nomeLoja, modelo);
912      if (!n) continue;
913      var dono = true;
914      for (var o = 0; o < outros.length; o++) {
915        if (chave(outros[o]) !== chave(modelo) && casaInicio(nomeLoja, outros[o]) > n) { dono = false; break; }
916      }
917      if (!dono) continue;
918      if (!por[med]) { por[med] = { medida: med, indiceNaLoja: idx || INDICE_LOJA[med] || '', versoes: [] }; ordem.push(med); }
919      por[med].versoes.push(nomeLoja);
920      vistas[nomeLoja] = 1;
921    }
922    for (var q = 0; q < ordem.length; q++) saida.medidas.push(por[ordem[q]]);
923    saida.medidas.sort(function (a, b) { return b.versoes.length - a.versoes.length; });
924    saida.versoesDaLoja = contaChaves(vistas);
925    return saida;
926  }
927
928  /** Os indices de carga e velocidade que a loja vende para uma medida. */
929  function indiceNaLoja(medida) {
930    pegaGlobais();
931    var m = lerMedida(medida);
932    if (!m) return '';
933    return INDICE_LOJA[m.normal] || INDICE_LOJA[m.normal.replace(/C$/, '')] || '';
934  }
935
936  function resumo() {
937    pegaGlobais();
938    var ger = 0, ver = 0, comIndice = 0, mods = {};
939    var cat = { marcas: {}, modelos: {}, geracoes: 0, versoes: 0 }, fic = { marcas: {}, modelos: {}, versoes: 0 };
940    for (var s in VEICULOS) {
941      var lista = entradas(s);
942      for (var i = 0; i < lista.length; i++) {
943        var km = s + '|' + chave(lista[i].n), vs = lista[i].vs || [];
944        ger++;
945        mods[km] = 1;
946        ver += vs.length;
947        if (lista[i].c) { cat.marcas[s] = 1; cat.modelos[km] = 1; cat.geracoes++; cat.versoes += vs.length; }
948        else { fic.marcas[s] = 1; fic.modelos[km] = 1; fic.versoes += vs.length; }
949        for (var j = 0; j < vs.length; j++) if (vs[j].i) comIndice++;
950      }
951    }
952    return { versao: VERSAO, marcas: contaChaves(MARCAS), marcasCarregadas: contaChaves(VEICULOS),
953             modelos: contaChaves(mods), geracoes: ger, versoes: ver,
954             comIndice: comIndice, medidas: medidasConhecidas().length, fontes: FONTES.length,
955             compatMarcas: contaChaves(COMPAT),
956             fichaTecnica: { marcas: contaChaves(fic.marcas), modelos: contaChaves(fic.modelos), versoes: fic.versoes },
957             catalogo: { ativo: CONFIG.catalogo, marcas: contaChaves(cat.marcas), modelos: contaChaves(cat.modelos),
958                         geracoes: cat.geracoes, versoes: cat.versoes } };
959  }
960
961  // ================================================================ [8] cascata no DOM
962  /** Liga quatro campos de selecao. Sem estilo e sem texto proprio: quem chama decide tudo.
963   *  opcoes: {marca, modelo, ano, versao} podem ser elemento ou seletor CSS.
964   *          rotulos: {marca:'...', modelo:'...', ano:'...', versao:'...'} para a opcao vazia.
965   *          onResultado(resultado, contexto) roda a cada escolha completa.
966   *          onEstado({carregando, etapa}) avisa quando esta carregando uma marca.
967   *          exigirVersao: false mostra o resultado assim que o ano e escolhido.
968   *          autoVersao: false nao escolhe sozinho a versao quando o ano so tem uma.
969   */
970  function ligarCascata(opcoes) {
971    opcoes = opcoes || {};
972    var doc = (typeof window !== 'undefined') ? window.document : null;
973    if (!doc) throw new Error('ligarCascata precisa de um documento');
974
975    function el(x) { return typeof x === 'string' ? doc.querySelector(x) : x; }
976    var cMarca = el(opcoes.marca), cModelo = el(opcoes.modelo);
977    var cAno = el(opcoes.ano), cVersao = el(opcoes.versao);
978    if (!cMarca || !cModelo || !cAno) throw new Error('ligarCascata precisa de marca, modelo e ano');
979    var rot = opcoes.rotulos || {};
980    var exigirVersao = opcoes.exigirVersao !== false && !!cVersao;
981    var estado = { marca: '', modelo: '', ano: 0, versao: '' };
982
983    function avisaEstado(carregando, etapa) {
984      if (opcoes.onEstado) opcoes.onEstado({ carregando: !!carregando, etapa: etapa || '' });
985    }
986
987    function enche(campo, itens, rotulo, valorDe, textoDe) {
988      if (!campo) return;
989      campo.innerHTML = '';
990      var vazio = doc.createElement('option');
991      vazio.value = '';
992      vazio.textContent = rotulo || '';
993      campo.appendChild(vazio);
994      for (var i = 0; i < itens.length; i++) {
995        var o = doc.createElement('option');
996        o.value = valorDe(itens[i]);
997        o.textContent = textoDe(itens[i]);
998        campo.appendChild(o);
999      }
1000      campo.disabled = itens.length === 0;
1001    }
1002
1003    function zera(campo, rotulo) {
1004      if (!campo) return;
1005      campo.innerHTML = '';
1006      var o = doc.createElement('option');
1007      o.value = ''; o.textContent = rotulo || '';
1008      campo.appendChild(o);
1009      campo.disabled = true;
1010    }
1011
1012    function dispara() {
1013      if (!estado.marca || !estado.modelo || !estado.ano) return;
1014      if (exigirVersao && !estado.versao) return;
1015      var r = buscar({ marca: estado.marca, modelo: estado.modelo, ano: estado.ano,
1016                       versao: exigirVersao ? estado.versao : '' });
1017      if (opcoes.onResultado) opcoes.onResultado(r, { estado: estado, motor: api });
1018    }
1019
1020    enche(cMarca, marcas(), rot.marca || '', function (m) { return m.slug; }, function (m) { return m.nome; });
1021    zera(cModelo, rot.modelo || '');
1022    zera(cAno, rot.ano || '');
1023    zera(cVersao, rot.versao || '');
1024
1025    cMarca.addEventListener('change', function () {
1026      estado.marca = cMarca.value; estado.modelo = ''; estado.ano = 0; estado.versao = '';
1027      zera(cModelo, rot.modelo || ''); zera(cAno, rot.ano || ''); zera(cVersao, rot.versao || '');
1028      if (!estado.marca) { avisaEstado(false, 'marca'); return; }
1029      avisaEstado(true, 'marca');
1030      carregar(estado.marca, function () {
1031        avisaEstado(false, 'marca');
1032        enche(cModelo, modelos(estado.marca), rot.modelo || '',
1033              function (m) { return m.nome; },
1034              function (m) { return m.nome + (opcoes.mostrarAnos ? ' (' + m.a1 + ' a ' + m.a2 + ')' : ''); });
1035      }, function (e) {
1036        avisaEstado(false, 'marca');
1037        if (opcoes.onErro) opcoes.onErro(e);
1038      });
1039    });
1040
1041    cModelo.addEventListener('change', function () {
1042      estado.modelo = cModelo.value; estado.ano = 0; estado.versao = '';
1043      zera(cAno, rot.ano || ''); zera(cVersao, rot.versao || '');
1044      if (!estado.modelo) return;
1045      enche(cAno, anos(estado.marca, estado.modelo), rot.ano || '',
1046            function (a) { return a; }, function (a) { return a; });
1047    });
1048
1049    cAno.addEventListener('change', function () {
1050      estado.ano = num(cAno.value); estado.versao = '';
1051      zera(cVersao, rot.versao || '');
1052      if (!estado.ano) return;
1053      // a ficha tecnica quando existe, senao o catalogo; uma opcao por versao, com todas as medidas dela
1054      var camadas = porCamada(versoes(estado.marca, estado.modelo, estado.ano));
1055      var lista = [], porNome = {};
1056      (camadas.ficha.length ? camadas.ficha : camadas.catalogo).forEach(function (v) {
1057        var item = porNome[v.versao];
1058        if (!item) { item = porNome[v.versao] = { versao: v.versao, medidas: [] }; lista.push(item); }
1059        var txt = v.medida + (v.eixo !== 'todos' ? ' ' + v.eixo : '');
1060        if (item.medidas.indexOf(txt) < 0) item.medidas.push(txt);
1061      });
1062      enche(cVersao, lista, rot.versao || '',
1063            function (v) { return v.versao; },
1064            function (v) { return v.versao + ': ' + v.medidas.join(' / '); });
1065      // opcoes.autoVersao:false deixa a pessoa escolher mesmo quando so ha uma versao (o nome pode nao ser o dela)
1066      if (cVersao && lista.length === 1 && opcoes.autoVersao !== false) {
1067        cVersao.value = lista[0].versao;
1068        estado.versao = lista[0].versao;
1069      }
1070      dispara();
1071    });
1072
1073    if (cVersao) {
1074      cVersao.addEventListener('change', function () {
1075        estado.versao = cVersao.value;
1076        dispara();
1077      });
1078    }
1079
1080    return {
1081      estado: estado,
1082      escolher: function (p) {
1083        p = p || {};
1084        if (p.marca) { cMarca.value = slugDaMarca(p.marca); cMarca.dispatchEvent(new Event('change')); }
1085        var seguir = function () {
1086          if (p.modelo) { cModelo.value = p.modelo; cModelo.dispatchEvent(new Event('change')); }
1087          if (p.ano) { cAno.value = String(p.ano); cAno.dispatchEvent(new Event('change')); }
1088          if (p.versao && cVersao) { cVersao.value = p.versao; cVersao.dispatchEvent(new Event('change')); }
1089        };
1090        if (p.marca) carregar(p.marca, seguir, seguir); else seguir();
1091      },
1092      zerar: function () {
1093        cMarca.value = ''; cMarca.dispatchEvent(new Event('change'));
1094      }
1095    };
1096  }
1097
1098  // ================================================================ saida
1099  var api = {
1100    VERSAO: VERSAO,
1101    INDICE_CARGA: INDICE_CARGA,
1102    SIMBOLO_VELOCIDADE: SIMBOLO_VELOCIDADE,
1103    configurar: configurar,
1104    usarDados: usarDados,
1105    carregar: carregar,
1106    carregarTudo: carregarTudo,
1107    marcas: marcas,
1108    modelos: modelos,
1109    geracoes: geracoes,
1110    anos: anos,
1111    versoes: versoes,
1112    buscar: buscar,
1113    porMedida: porMedida,
1114    medidasConhecidas: medidasConhecidas,
1115    sugerir: sugerir,
1116    resumo: resumo,
1117    lerMedida: lerMedida,
1118    medidaValida: medidaValida,
1119    normalizarMedida: normalizarMedida,
1120    lerIndice: lerIndice,
1121    cargaDoIndice: cargaDoIndice,
1122    velocidadeDoSimbolo: velocidadeDoSimbolo,
1123    indiceParaCarga: indiceParaCarga,
1124    comparar: comparar,
1125    equivalentes: equivalentes,
1126    ligarCascata: ligarCascata,
1127    compativeis: compativeis,
1128    indiceNaLoja: indiceNaLoja,
1129    // ajudantes expostos porque quem integra costuma precisar
1130    utilidades: { slug: slug, chave: chave, semAcento: semAcento, num: num, arred: arred, unico: unico }
1131  };
1132  return api;
1133}));

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.