Другое

Как исправить проблемы с загрузкой GeoJSON в SVG

Исправьте неработающую функцию загрузки SVG в вашем приложении для конвертации GeoJSON. Узнайте о кросс-браузерных решениях для Blob URL, MIME-типов и методов резервного копирования.

Как исправить функциональность загрузки в веб-приложении для преобразования GeoJSON в SVG? Я создал веб-приложение, которое преобразует данные полигонов GeoJSON в формат SVG с помощью JavaScript. Приложение корректно отображает визуализацию SVG, но кнопка загрузки не запускает скачивание. Код включает проекцию координат из lon/lat в Web Mercator, расчеты подгонки области просмотра и рендеринг SVG. Вот полная реализация:

javascript
function lonLatToWebMercator(lon, lat) {
  // возвращает {x, y} в метрах (EPSG:3857)
  const R = 6378137;
  const x = R * lon * Math.PI / 180;
  const latRad = lat * Math.PI / 180;
  const y = R * Math.log(Math.tan(Math.PI / 4 + latRad / 2));
  return { x, y };
}

function projectPolygon(coords) {
  // coords: массив линейных колец; используем внешнее кольцо (coords[0])
  const ring = coords[0];
  return ring.map(([lon, lat]) => lonLatToWebMercator(lon, lat));
}

function buildPathString(points) {
  if (!points.length) return '';
  return points.map((p, i) => (i === 0 ? 'M' : 'L') + p.x.toFixed(2) + ' ' + p.y.toFixed(2)).join(' ') + ' Z';
}

function fitToViewport(points, width, height, padding = 20) {
  // points: массив {x,y} в проекционных единицах
  const xs = points.map(p => p.x), ys = points.map(p => p.y);
  const minx = Math.min(...xs), maxx = Math.max(...xs);
  const miny = Math.min(...ys), maxy = Math.max(...ys);
  const dx = maxx - minx || 1;
  const dy = maxy - miny || 1;

  // масштабирование с сохранением пропорций
  const availW = width - 2 * padding, availH = height - 2 * padding;
  const scale = Math.min(availW / dx, availH / dy);

  // сдвиг, чтобы minx, maxy соответствовали padding, padding (мы перевернем y)
  // SVG y увеличивается вниз, но Mercator y увеличивается вверх.
  // Мы отображаем mercator y так, чтобы большее y соответствовало меньшему SVG y: svgY = (maxy - y)*scale + padding
  const tx = -minx * scale + padding;
  const ty = -maxy * scale + padding; // используется с переворотом в отображении

  const mapped = points.map(p => {
    return {
      x: p.x * scale + tx,
      y: (p.y * -1) * scale - ty + height // проще: вычисляем y как (maxy - p.y)*scale + padding
    };
  });

  // Для более ясного расчета делаем правильное отображение ниже:
  const mapped2 = points.map(p => {
    const sx = (p.x - minx) * scale + padding;
    const sy = (maxy - p.y) * scale + padding; // переворачиваем Y
    return { x: sx, y: sy };
  });

  return {
    mapped: mapped2,
    minx, maxx, miny, maxy,
    scale,
    padding
  };
}

function renderGeoJSONToSVG(geojson, width=640, height=420) {
  if (!geojson || !geojson.geometry) throw new Error('Invalid GeoJSON');

  const geom = geojson.geometry;
  if (geom.type !== 'Polygon') throw new Error('Only Polygon geometry is supported in this demo');

  const proj = projectPolygon(geom.coordinates);
  const fit = fitToViewport(proj, width, height, 20);
  const mapped = fit.mapped;

  // строим строку пути из отображенных координат
  const d = mapped.map((p,i) => (i===0 ? 'M' : 'L') + p.x.toFixed(2) + ' ' + p.y.toFixed(2)).join(' ') + ' Z';

  // создаем svg элемент
  const ns = 'http://www.w3.org/2000/svg';
  const svg = document.createElementNS(ns, 'svg');
  svg.setAttribute('width', width);
  svg.setAttribute('height', height);
  svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
  svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
  svg.style.background = '#071016';

  // defs (необязательные стили)
  const defs = document.createElementNS(ns, 'defs');
  svg.appendChild(defs);

  // путь полигона
  const path = document.createElementNS(ns, 'path');
  path.setAttribute('d', d);
  path.setAttribute('fill', 'rgba(0,180,120,0.15)');
  path.setAttribute('stroke', '#00b488');
  path.setAttribute('stroke-width', '2');
  path.setAttribute('stroke-linejoin', 'round');
  svg.appendChild(path);

  // рисуем точки
  mapped.forEach(p => {
    const c = document.createElementNS(ns, 'circle');
    c.setAttribute('cx', p.x.toFixed(2));
    c.setAttribute('cy', p.y.toFixed(2));
    c.setAttribute('r', '3');
    c.setAttribute('fill', '#fff');
    c.setAttribute('stroke', '#007f5f');
    svg.appendChild(c);
  });

  // текст информации о bbox
  const info = document.createElementNS(ns, 'text');
  info.setAttribute('x', 10);
  info.setAttribute('y', height - 8);
  info.setAttribute('fill', '#9ca3af');
  info.setAttribute('font-size', '11');
  info.textContent = `scale: ${fit.scale.toFixed(6)}  bbox: [${fit.minx.toFixed(2)},${fit.miny.toFixed(2)}] - [${fit.maxx.toFixed(2)},${fit.maxy.toFixed(2)}]`;
  svg.appendChild(info);

  return svg;
}

// подключение UI
const textarea = document.getElementById('gj');
const container = document.getElementById('svgContainer');
const meta = document.getElementById('meta');

function doRender() {
  container.innerHTML = '';
  meta.textContent = '';
  let g;
  try {
    g = JSON.parse(textarea.value.trim());
  } catch (e) {
    meta.textContent = 'Invalid JSON: ' + e.message;
    return;
  }
  try {
    const svg = renderGeoJSONToSVG(g, container.clientWidth, container.clientHeight);
    container.appendChild(svg);
    meta.textContent = 'Rendered polygon (projected via Web Mercator).';
  } catch (e) {
    meta.textContent = 'Error: ' + e.message;
  }
}

document.getElementById('render').addEventListener('click', doRender);

// Загрузка SVG
document.getElementById('download').addEventListener('click', () => {
  const svgEl = container.querySelector('svg');
  if (!svgEl) { alert('No SVG to download'); return; }
  const serializer = new XMLSerializer();
  const svgStr = serializer.serializeToString(svgEl);
  const blob = new Blob([svgStr], {type: 'image/svg+xml'});
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'polygon.svg';
  a.click();
  URL.revokeObjectURL(url);
});

// начальный рендеринг
doRender();

HTML-структура включает текстовое поле для ввода GeoJSON, кнопку рендеринга, кнопку загрузки и контейнер для вывода SVG. Кнопка загрузки должна запускать скачивание сгенерированного SVG, но она работает не так, как ожидалось. Что может быть причиной этой проблемы, и как можно её исправить?

Краткий ответ

Функциональность загрузки не работает из-за несовместимости браузеров с Blob URL и неправильного указания MIME типа. В частности, Safari отклоняет image/svg+xml;charset=utf-8 и предпочитает просто image/svg+xml, а некоторые браузеры могут иметь проблемы с немедленным отзывом URL. Реализация кросс-браузерного решения с соответствующей обработкой ошибок и механизмами резервного копирования решит эту проблему.

Содержание

Распространенные причины сбоев при загрузке SVG

На основе исследования, несколько факторов могут вызывать сбои при загрузке SVG:

  1. Неправильный MIME тип: Safari не принимает image/svg+xml;charset=utf-8 и работает только с image/svg+xml источник

  2. Преждевременный отзыв URL: Отзыв Blob URL сразу после запуска загрузки может вызвать проблемы в некоторых браузерах

  3. Совместимость с браузерами: Разные браузеры по-разному обрабатывают Blob URL, особенно старые версии и мобильные браузеры

  4. Кросс-доменная безопасность: SVG элементы, содержащие внешние ресурсы, могут вызывать ограничения безопасности

  5. Поддержка атрибута загрузки: Не все браузеры полностью поддерживают атрибут download

Кросс-браузерное совместимое решение

Вот улучшенная функция загрузки, которая решает эти проблемы:

javascript
function downloadSVG(svgElement, filename = 'polygon.svg') {
  try {
    const serializer = new XMLSerializer();
    const svgStr = serializer.serializeToString(svgElement);
    
    // Создание нескольких методов резервного копирования
    const downloadMethods = [
      // Метод 1: Blob URL с правильным MIME типом
      () => {
        const blob = new Blob([svgStr], { type: 'image/svg+xml' });
        return URL.createObjectURL(blob);
      },
      // Метод 2: Резервный вариант с data URL
      () => {
        return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgStr);
      },
      // Метод 3: Резервный вариант с Base64
      () => {
        return 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgStr)));
      }
    ];

    let downloadUrl = null;
    let successfulMethod = -1;

    // Пробуем каждый метод до тех пор, пока один не сработает
    for (let i = 0; i < downloadMethods.length; i++) {
      try {
        downloadUrl = downloadMethods[i]();
        
        const a = document.createElement('a');
        a.href = downloadUrl;
        a.download = filename;
        
        // Проверяем, поддерживается ли метод, проверяя поддержку атрибута download
        const supportsDownload = 'download' in a;
        if (!supportsDownload) {
          throw new Error('Атрибут download не поддерживается');
        }
        
        // Запускаем загрузку
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        
        successfulMethod = i;
        break;
      } catch (e) {
        console.warn(`Метод загрузки ${i + 1} не сработал:`, e);
        if (downloadUrl) {
          URL.revokeObjectURL(downloadUrl);
        }
        continue;
      }
    }

    // Очищаем Blob URL, если он был создан
    if (successfulMethod === 0 && downloadUrl) {
      // Используем setTimeout, чтобы позволить загрузке начаться перед отзывом URL
      setTimeout(() => {
        URL.revokeObjectURL(downloadUrl);
      }, 100);
    }

    if (successfulMethod === -1) {
      throw new Error('Все методы загрузки не сработали');
    }

    return true;
  } catch (error) {
    console.error('Ошибка загрузки SVG:', error);
    alert(`Ошибка загрузки: ${error.message}. Пожалуйста, попробуйте щелкнуть правой кнопкой мыши по SVG и выбрать "Сохранить изображение как..."`);
    return false;
  }
}

// Обновленный обработчик события загрузки
document.getElementById('download').addEventListener('click', () => {
  const svgEl = container.querySelector('svg');
  if (!svgEl) { 
    alert('Нет SVG для загрузки'); 
    return; 
  }
  
  // Добавляем метку времени к имени файла, чтобы избежать конфликтов
  const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
  const filename = `polygon-${timestamp}.svg`;
  
  downloadSVG(svgEl, filename);
});

Специфические для браузера обходные пути

Специфические исправления для Safari

javascript
function isSafari() {
  return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
}

function getSafariSafeDownloadMethod(svgStr) {
  // Safari предпочитает data URI вместо Blob URL
  return 'data:image/svg+xml,' + encodeURIComponent(svgStr);
}

Совместимость с Internet Explorer

javascript
function isIE() {
  return /*@cc_on!@*/false || !!document.documentMode;
}

function getIECompatibleDownloadMethod(svgStr) {
  // IE имеет проблемы с Blob URL, используем data URI
  return 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgStr)));
}

Дополнительные шаги по устранению неполадок

  1. Проверьте консоль браузера: Откройте инструменты разработчика браузера и проверьте наличие сообщений об ошибках при нажатии на загрузку

  2. Протестируйте разные имена файлов: Попробуйте удалить специальные символы из имени файла

  3. Проверьте содержимое SVG: Убедитесь, что SVG не содержит недопустимых символов или внешних ссылок

  4. Протестируйте в режиме инкогнито: Некоторые расширения браузера могут мешать загрузкам

  5. Проверьте размер файла: Очень большие SVG файлы могут вызывать проблемы в некоторых браузерах

Полная реализация

Вот полная обновленная функциональность загрузки, интегрированная с вашим существующим кодом:

javascript
// Улучшенная функция загрузки с кросс-браузерной поддержкой
function downloadSVG(svgElement, filename = 'polygon.svg') {
  try {
    const serializer = new XMLSerializer();
    const svgStr = serializer.serializeToString(svgElement);
    
    // Определение браузера
    const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
    const isIE = /*@cc_on!@*/false || !!document.documentMode;
    
    let downloadUrl;
    
    if (isSafari || isIE) {
      // Используем data URI для Safari и IE
      downloadUrl = 'data:image/svg+xml,' + encodeURIComponent(svgStr);
    } else {
      // Используем Blob URL для современных браузеров
      const blob = new Blob([svgStr], { type: 'image/svg+xml' });
      downloadUrl = URL.createObjectURL(blob);
    }
    
    const a = document.createElement('a');
    a.href = downloadUrl;
    a.download = filename;
    
    // Резервный вариант для браузеров, которые не поддерживают атрибут download
    if (!('download' in a)) {
      const newWindow = window.open(downloadUrl, '_blank');
      if (!newWindow || newWindow.closed || typeof newWindow.closed === 'boolean') {
        throw new Error('Всплывающее окно заблокировано или загрузка не поддерживается');
      }
      return;
    }
    
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    
    // Очищаем Blob URL, если он был создан
    if (!isSafari && !isIE && downloadUrl.startsWith('blob:')) {
      setTimeout(() => {
        URL.revokeObjectURL(downloadUrl);
      }, 100);
    }
    
    return true;
  } catch (error) {
    console.error('Ошибка загрузки SVG:', error);
    
    // Резервный вариант: открыть в новой вкладке для ручной загрузки
    try {
      const svgEl = container.querySelector('svg');
      if (svgEl) {
        const serializer = new XMLSerializer();
        const svgStr = serializer.serializeToString(svgEl);
        const dataUrl = 'data:image/svg+xml,' + encodeURIComponent(svgStr);
        window.open(dataUrl, '_blank');
        return true;
      }
    } catch (fallbackError) {
      console.error('Резервная загрузка также не сработала:', fallbackError);
    }
    
    alert(`Ошибка загрузки. Пожалуйста, щелкните правой кнопкой мыши по визуализации SVG и выберите "Сохранить изображение как..."`);
    return false;
  }
}

// Обновленный обработчик события загрузки
document.getElementById('download').addEventListener('click', () => {
  const svgEl = container.querySelector('svg');
  if (!svgEl) { 
    alert('Нет SVG для загрузки'); 
    return; 
  }
  
  // Генерируем имя файла с меткой времени
  const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
  const filename = `geojson-polygon-${timestamp}.svg`;
  
  // Пытаемся загрузить
  const success = downloadSVG(svgEl, filename);
  
  if (success) {
    meta.textContent = 'SVG успешно загружен!';
    setTimeout(() => {
      meta.textContent = 'Отображенный полигон (спроецирован через Web Mercator).';
    }, 3000);
  }
});

Это улучшенное решение предоставляет несколько механизмов резервного копирования, обрабатывает особенности конкретных браузеров и включает соответствующую обработку ошибок для обеспечения работы вашей функциональности загрузки SVG в разных браузерах и средах.

Источники

  1. Stack Overflow - BLOB URL не работает в Safari
  2. Stack Overflow - Загрузка SVG файла по клику на ссылку
  3. Mozilla Discourse - Blob как источник изображения в SVG
  4. Reddit - Загрузка SVG файла с JavaScript
  5. W3C SVGWG - Blob URL в SVG как ссылки

Вывод

Основные выводы для исправления функциональности загрузки SVG:

  1. Исправьте MIME тип: Используйте image/svg+xml без charset для совместимости с Safari
  2. Реализуйте несколько резервных копий: методы Blob URL, data URI и base64 для разных браузеров
  3. Обрабатывайте определение браузера: Safari и IE требуют специального подхода
  4. Добавьте обработку ошибок: плавное понижение при сбое загрузки
  5. Задержите отзыв URL: используйте setTimeout для отзыва Blob URL после начала загрузки

Улучшенная реализация должна решить ваши проблемы с загрузкой, сохраняя совместимость с современными браузерами. Для лучшего пользовательского опыта рассмотрите возможность добавления визуальной обратной связи в процессе загрузки и предоставления четких инструкций по резервному копированию, когда автоматическая загрузка не поддерживается.

Авторы
Проверено модерацией
Модерация