Как исправить проблемы с загрузкой GeoJSON в SVG
Исправьте неработающую функцию загрузки SVG в вашем приложении для конвертации GeoJSON. Узнайте о кросс-браузерных решениях для Blob URL, MIME-типов и методов резервного копирования.
Как исправить функциональность загрузки в веб-приложении для преобразования GeoJSON в SVG? Я создал веб-приложение, которое преобразует данные полигонов GeoJSON в формат SVG с помощью JavaScript. Приложение корректно отображает визуализацию SVG, но кнопка загрузки не запускает скачивание. Код включает проекцию координат из lon/lat в Web Mercator, расчеты подгонки области просмотра и рендеринг SVG. Вот полная реализация:
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
На основе исследования, несколько факторов могут вызывать сбои при загрузке SVG:
-
Неправильный MIME тип: Safari не принимает
image/svg+xml;charset=utf-8и работает только сimage/svg+xmlисточник -
Преждевременный отзыв URL: Отзыв Blob URL сразу после запуска загрузки может вызвать проблемы в некоторых браузерах
-
Совместимость с браузерами: Разные браузеры по-разному обрабатывают Blob URL, особенно старые версии и мобильные браузеры
-
Кросс-доменная безопасность: SVG элементы, содержащие внешние ресурсы, могут вызывать ограничения безопасности
-
Поддержка атрибута загрузки: Не все браузеры полностью поддерживают атрибут download
Кросс-браузерное совместимое решение
Вот улучшенная функция загрузки, которая решает эти проблемы:
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
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
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)));
}
Дополнительные шаги по устранению неполадок
-
Проверьте консоль браузера: Откройте инструменты разработчика браузера и проверьте наличие сообщений об ошибках при нажатии на загрузку
-
Протестируйте разные имена файлов: Попробуйте удалить специальные символы из имени файла
-
Проверьте содержимое SVG: Убедитесь, что SVG не содержит недопустимых символов или внешних ссылок
-
Протестируйте в режиме инкогнито: Некоторые расширения браузера могут мешать загрузкам
-
Проверьте размер файла: Очень большие SVG файлы могут вызывать проблемы в некоторых браузерах
Полная реализация
Вот полная обновленная функциональность загрузки, интегрированная с вашим существующим кодом:
// Улучшенная функция загрузки с кросс-браузерной поддержкой
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 в разных браузерах и средах.
Источники
- Stack Overflow - BLOB URL не работает в Safari
- Stack Overflow - Загрузка SVG файла по клику на ссылку
- Mozilla Discourse - Blob как источник изображения в SVG
- Reddit - Загрузка SVG файла с JavaScript
- W3C SVGWG - Blob URL в SVG как ссылки
Вывод
Основные выводы для исправления функциональности загрузки SVG:
- Исправьте MIME тип: Используйте
image/svg+xmlбез charset для совместимости с Safari - Реализуйте несколько резервных копий: методы Blob URL, data URI и base64 для разных браузеров
- Обрабатывайте определение браузера: Safari и IE требуют специального подхода
- Добавьте обработку ошибок: плавное понижение при сбое загрузки
- Задержите отзыв URL: используйте setTimeout для отзыва Blob URL после начала загрузки
Улучшенная реализация должна решить ваши проблемы с загрузкой, сохраняя совместимость с современными браузерами. Для лучшего пользовательского опыта рассмотрите возможность добавления визуальной обратной связи в процессе загрузки и предоставления четких инструкций по резервному копированию, когда автоматическая загрузка не поддерживается.