یکی از مشکلات استفاده از نقشههای رایگان در وباپلیکیشنهای ایرانی، نمایش عبارت «خلیج عربی» به جای «خلیج پارس» هست و در پشت پرده نیز کشمکش بین برنامهنویسهای ایرانی و عرب در این رابطه وجود داشته و گاهی اوقات جامعه برنامهنویسهای ایرانی توانسته تیم پشتیبان سرویس میزبانی نقشه OpenStreetMap رو برای نمایش عبارت «خلیج پارس» یا «خلیج فارس» مجاب کنه؛ اگر چه به نظر نمیرسه جنگ بر سر نام این خلیج به این زودیها به نفع ایران تموم بشه.
به طور کلی، رفع چنین مشکلی فقط با استفاده از Tile Server اختصاصی یا سرویسهای نقشه غیررایگان مثل پارسیجو یا نشان ممکن هست!
اخیراً در وباپلیکیشن جدید جاجیگا به طور مستقیم با این چالش روبرو شدم و در نهایت تونستم به روش جدیدی برای جایگزینی عبارت «خیلج عربی» با «خلیج پارس» دست پیدا کنم.
در این روش، از سرویس وُرکر (ServiceWorker) به عنوان یک پروکسی برای جایگزینی تایل(Tile)های محدوده خلیج پارس با تصاویر یا تایلهای ویرایششده استفاده کردم که در ادامه به تشریح این روش خواهم پرداخت.
در قدم اول، وباپلیکیشن شما میبایست از سرویس وُرکر برخوردار بوده و امکان کش فایلهای استاتیک توسط آن فراهم شده باشه.
تمامی تایلهای حاوی عبارت «خلیج عربی» یا بخشی از آن رو دانلود نموده و با استفاده از ادیتور تصویری همچون فتوشاپ به ویرایش و جایگزینی تک تک تایلها میپردازیم. دقت کنین که برای کلیه Zoom Levelها تایلهای مربوطه را در نظر بگیرین.
فهرست تایلها جهت ویرایش:
https://b.tile.openstreetmap.org/6/41/26.png https://a.tile.openstreetmap.org/6/40/26.png https://a.tile.openstreetmap.org/5/20/13.png https://a.tile.openstreetmap.org/7/82/53.png https://b.tile.openstreetmap.org/8/164/107.png
در اینجا میبینیم که آدرس هر تایل دارای ساختار مشخصی هست:
https://[abc].tile.openstreetmap.org/{z}/{x}/{y}.png
در ساختار بالا متغیرها عبارتاند از (جزئیات بیشتر را در ویکیپدیا مطالعه کنید):
پارامتر abc: نام تایل سرور (یکی از حروف c ،b ،a)
پارامتر z: سطح بزرگنمایی (Zoom Level)
پارامتر x: مؤلفه مکانی افقی
پارامتر y: مؤلفه مکانی عمودی
با توجه به این ساختار میخواهیم برای تصاویر تایلهای ویرایششده filename مشخصی در نظر بگیریم تا تناظری بین آدرس اصلی تایل و آدرس تصویر ویرایششده روی سایتمون ایجاد بشه.
یک تناظر ساده به صورت زیر خواهد بود:
https://www.jajiga.com/static/map/[abc]-{z}-{x}-{y}.png
با این تناظر، تصاویر تایل در پوشه استاتیک پروژه من، به صورت زیر خواهد بود:
/static/map/b-6-41-26.png /static/map/a-6-40-26.png /static/map/a-5-20-13.png /static/map/a-7-82-53.png /static/map/b-8-164-107.png
در مرحله قبل تایلهای مربوط به ناحیه خلیج عربی رو شخصیسازی و در آخر با استفاده از یک قاعده مشخص، اونها رو نامگذاری کردیم. از اینجا به بعد دست به کد میشیم و در سرویس وُرکر اپمون امکانی فراهم میکنیم تا درخواستهای مربوط به تصاویر تایل Leaflet (که از SW میگذرن) به سمت تصاویر شخصیسازیشده موجود هدایت بشن.
قطعه کد زیر رو در نظر بگیرین:
const CACHE_KEY = 'example-v1'; const PRECACHE_URLS = [ '/offline.html', '/favicon.ico', //... ]; self.addEventListener('install', (event) => { event.waitUntil( caches .open(CACHE_KEY) .then((cache) => cache.addAll(PRECACHE_URLS)) .then(self.skipWaiting()), ); }); const fetchFromCache = async (request) => { const latestCache = await caches.open(CACHE_KEY); if (!latestCache) return null; const response = await latestCache.match(request); return response; }; const onFetch = async (event) => { const url = new URL(event.request.url); if (PRECACHE_URLS.includes(url.pathname)) { return event.respondWith( fetchFromCache(event.request) .then((response) => { if (response) { return response; } return fetch(event.request); }) .catch((error) => { console.log(error); }), ); } return null; }; self.addEventListener('fetch', onFetch);
همانطور که میدونید وقتی وب اپلیکیشن ما از Service Worker برخوردار میشه، همه درخواستهای HTTP از این سرویس عبور میکنن و میتونیم به هر نحوی به این درخواستها پاسخ بدیم.
قطعه کد بالا، یک Service Worker ساده رو برای پیشکَشکردن (Precache) برخی فایلهای استاتیک پروژهمون ایجاد میکنه و در ادامه قصد داریم کد رو به گونهای تغییر بدیم که SW، تصاویر تایل شخصیسازیشده رو ابتدا Precache کنه و بعد هنگام دریافت درخواستهای HTTP مربوط به تایل نقشه، فایل تصویر متناظر رو بارگذاری کنه.
پس ابتدا، تایلهای جدید رو به فهرست فایلهای قابلکَش اضافه میکنیم:
const PRECACHE_URLS = [ '/offline.html', '/favicon.ico', //... '/static/map/b-6-41-26.png', '/static/map/a-6-40-26.png', '/static/map/a-5-20-13.png', '/static/map/a-7-82-53.png', '/static/map/b-8-164-107.png', //... ];
در قدم بعدی، تابع onFetch رو به صورت زیر تغییر میدیم:
const onFetch = async (event) => { const url = new URL(event.request.url); // Proxy leaflet map tiles if (url.origin.includes('openstreetmap')) { const response = getPersianGulfTile(url.href); if (response) { return event.respondWith(response); } } //... return null; };
در قطعه کد بالا، ابتدا درخواستهای مربوط به تایل نقشه رو تشخیص دادم (url چنین درخواستهای حاوی عبارت openstreetmap هست) و بعد تابعی برای ایجاد پاسخ مناسب در نظر گرفتم.
ورودی این تابع همون آدرس تایل اصلی و خروجی این تابع قراره همون تایلهای ویرایششده باشه.
حالا به تعریف تابع getPersianGulfTile میپردازیم:
const getPersianGulfTile = (tileHref) => { const tileURL = new URL(tileHref); const localTileUrl = '/static/map/' + [tileURL.host.split('.')[0], ...tileURL.pathname.slice(1).split('/')].join('-'); if (PRECACHE_URLS.includes(localTileUrl)) { return fetchFromCache(new Request(localTileUrl)); } };
همانطور که میبینید، در خط 3، مقدار url جدید بر اساس الگوی تناظری که مشخص کرده بودیم، تعریف شده. در ادامه، تصویر تایل موجود در Cache به عنوان تایل جدید برمیگرده.
حالا نگاهی به کد نهایی بندازیم:
const CACHE_KEY = 'example-v1'; const PRECACHE_URLS = [ '/offline.html', '/favicon.ico', //... '/static/map/b-6-41-26.png', '/static/map/a-6-40-26.png', '/static/map/a-5-20-13.png', '/static/map/a-7-82-53.png', '/static/map/b-8-164-107.png', //... ]; self.addEventListener('install', (event) => { event.waitUntil( caches .open(CACHE_KEY) .then((cache) => cache.addAll(PRECACHE_URLS)) .then(self.skipWaiting()), ); }); const fetchFromCache = async (request) => { const latestCache = await caches.open(CACHE_KEY); if (!latestCache) return null; const response = await latestCache.match(request); return response; }; const getPersianGulfTile = (tileHref) => { const tileURL = new URL(tileHref); const localTileUrl = '/static/map/' + [tileURL.host.split('.')[0], ...tileURL.pathname.slice(1).split('/')].join('-'); if (PRECACHE_URLS.includes(localTileUrl)) { return fetchFromCache(new Request(localTileUrl)); } }; const onFetch = async (event) => { const url = new URL(event.request.url); // Proxy leaflet map tiles if (url.origin.includes('openstreetmap')) { const response = getPersianGulfTile(url.href); if (response) { return event.respondWith(response); } } if (PRECACHE_URLS.includes(url.pathname)) { return event.respondWith( fetchFromCache(event.request) .then((response) => { if (response) { return response; } return fetch(event.request); }) .catch((error) => { console.log(error); }), ); } return null; }; self.addEventListener('fetch', onFetch);
خیلی جالبه :) به یاری Service Worker تونستیم تصاویر تایل «خلیج عربی» رو با تایلهای جدید «خلیج پارس» جایگزین کنیم یا به عبارتی تصاویر نقشه رو پروکسی کردیم.
شاد باشید
***************************
خلیج پارس، نامت پایدار ...
ای همیشه نیلگون، ای خلیج پارس، نام نامیات همواره جاودانه است
نام نامیات همواره بر زبانِ موج، جاودانترین سرود عاشقانه است