How to remove borders of other countries and display only India on the map?
Hi OpenLayers team,
I'm working on a React project using OpenLayers and trying to restrict the map view to India only. I have India-specific layers like states, districts, and subdistricts (in TopoJSON format), which work well. However, the base layer (Google Maps via XYZ tiles) still shows borders of other countries, which I would like to hide or mask.
Objective:
I want to hide all international country borders except for India (i.e., prevent borders or features of neighboring countries from being visible). The goal is to keep the context strictly within India for the users.
Current Setup:
I'm using multiple layers, including:
- Google Maps base layers via XYZ tile source
- India-specific vector layers using TopoJSON for states/districts/subdistricts
- Magnify overlay for interactive zoom
Here's my code
import React, { useEffect, useRef } from 'react';
import 'ol/ol.css';
import 'ol-ext/dist/ol-ext.css';
const MapComponent = ({ onLocationSelect }) => {
const mapRef = useRef(null);
const magnifyRef = useRef(null);
useEffect(() => {
// Dynamically import OpenLayers modules to ensure they only run in browser environment
import('ol/Map').then(({ default: Map }) => {
import('ol/View').then(({ default: View }) => {
import('ol/layer/Tile').then(({ default: TileLayer }) => {
import('ol/source/XYZ').then(({ default: XYZ }) => {
import('ol/proj').then(({ fromLonLat, toLonLat }) => {
import('ol-ext/control/LayerSwitcher').then(
({ default: LayerSwitcher }) => {
import('ol-ext/overlay/Magnify').then(
({ default: Magnify }) => {
import('ol/layer/Vector').then(
({ default: VectorLayer }) => {
import('ol/source/Vector').then(
({ default: VectorSource }) => {
import('ol/format/GeoJSON').then(
({ default: GeoJSON }) => {
import('ol/style/Style').then(
({ default: Style }) => {
import('ol/style/Stroke').then(
({ default: Stroke }) => {
import('ol/style/Fill').then(
({ default: Fill }) => {
import(
'ol/format/TopoJSON'
).then(
({ default: TopoJSON }) => {
// Create options objects separately from layer creation to support properties like 'title'
// that aren't in the TypeScript definitions
// Google Maps base layer
const googleMapsOptions = {
source: new XYZ({
url: 'https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}',
attributions:
'© Google Maps',
maxZoom: 20,
}),
};
// Use type assertion to add the title property
const googleMapsLayer =
new TileLayer(
googleMapsOptions
);
// Add title as a property to the layer object
googleMapsLayer.set(
'title',
'Google Maps'
);
// Google Satellite layer
const googleSatelliteOptions =
{
source: new XYZ({
url: 'https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}',
attributions:
'© Google Maps',
maxZoom: 20,
}),
visible: false,
};
const googleSatelliteLayer =
new TileLayer(
googleSatelliteOptions
);
googleSatelliteLayer.set(
'title',
'Google Satellite'
);
// Google Hybrid layer
const googleHybridOptions = {
source: new XYZ({
url: 'https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}',
attributions:
'© Google Maps',
maxZoom: 20,
}),
visible: false,
};
const googleHybridLayer =
new TileLayer(
googleHybridOptions
);
googleHybridLayer.set(
'title',
'Google Hybrid'
);
// India States Vector Layer
const indiaStatesSource =
new VectorSource({
url: '/topoJsons/india_states.json',
format: new TopoJSON(),
});
const indiaStatesLayer =
new VectorLayer({
source:
indiaStatesSource,
style: new Style({
stroke: new Stroke({
color: '#3388ff',
width: 1,
}),
fill: new Fill({
color:
'rgba(255, 255, 255, 0.1)',
}),
}),
// visible: false,
});
indiaStatesLayer.set(
'title',
'India States'
);
// India Districts Vector Layer
const indiaDistrictSource =
new VectorSource({
url: '/topoJsons/india_districts.json',
format: new TopoJSON(),
});
const indiaDistrictsLayer =
new VectorLayer({
source:
indiaDistrictSource,
style: new Style({
stroke: new Stroke({
color: '#FF0000',
width: 1,
}),
fill: new Fill({
color:
'rgba(255, 255, 255, 0.1)',
}),
}),
visible: false,
});
indiaDistrictsLayer.set(
'title',
'India Districts'
);
// India Subdistricts Vector Layer
const indiaSubdistrictSource =
new VectorSource({
url: '/topoJsons/india_subdistricts.json',
format: new TopoJSON(),
});
const indiaSubdistrictsLayer =
new VectorLayer({
source:
indiaSubdistrictSource,
style: new Style({
stroke: new Stroke({
color: '#FFFF00',
width: 1,
}),
fill: new Fill({
color:
'rgba(255, 255, 255, 0.1)',
}),
}),
visible: false,
});
indiaSubdistrictsLayer.set(
'title',
'India Subdistricts'
);
const map = new Map({
target: mapRef.current,
layers: [
googleMapsLayer,
googleSatelliteLayer,
googleHybridLayer,
indiaStatesLayer,
indiaDistrictsLayer,
indiaSubdistrictsLayer,
],
view: new View({
// Adjusted center to precisely cover India
center: fromLonLat([
80.9, // Longitude adjusted to center India
22.5, // Latitude adjusted to center India
]),
zoom: 4, // Reduced zoom to show full country
// Uncomment and adjust the extent to restrict the view to India
extent: fromLonLat([
31.0, // Western longitude (slightly adjusted for full coverage)
5.5, // Southern latitude (slightly adjusted for full coverage)
]).concat(
fromLonLat([
107.5, // Eastern longitude (slightly adjusted for full coverage)
57.5, // Northern latitude (adjusted to include entire northern region)
])
),
// Optional: Add min and max zoom constraints
minZoom: 4,
maxZoom: 8,
}),
});
const switcher =
new LayerSwitcher();
map.addControl(switcher);
// Magnify overlay
const magnifySource =
new XYZ({
url: 'https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}',
attributions:
'© Google Maps',
maxZoom: 20,
});
const magnifyLayer =
new TileLayer({
source: magnifySource,
});
const magnify = new Magnify({
layers: [magnifyLayer],
radius: 120,
zoomOffset: 1,
rotateWithView: false,
stabilized: true,
});
map.addOverlay(magnify);
magnifyRef.current = magnify;
map.on(
'pointermove',
(evt) => {
magnify.setPosition(
evt.coordinate
);
}
);
map.on('click', (evt) => {
const lonLat = toLonLat(
evt.coordinate
);
// Pass the click coordinates to the parent component
onLocationSelect({
lat: lonLat[1],
lon: lonLat[0],
});
});
setTimeout(() => {
map.updateSize();
}, 100);
}
);
}
);
}
);
}
);
}
);
}
);
}
);
}
);
}
);
});
});
});
});
});
return () => {
// Cleanup function to prevent memory leaks
if (mapRef.current) {
mapRef.current.innerHTML = '';
}
};
}, []);
return (
<div
ref={mapRef}
className="map-container"
style={{
width: '100%',
margin: 'auto',
height: '750px',
border: '1px solid #ccc',
}}
/>
);
};
export default MapComponent;
Any help or example would be much appreciated!
Thanks for this amazing library and all your support.
hi
- so if i understand in fact you want a google ressource with satellite and label but without the administrative boundary from goggle ? (so satellite is ok but label (hybride) have also the admin ...) i have never experimented this and dont know but i think that google have not a ressource xyz for this ??
I believe this example is what you are looking for: Vector Clipping Layer. Basically, you just need to provide a vector layer that is the boundary for India and on the post render of the tiles you will applying the India vector layer boundary. In the examples case they have an extra layer under the layer they are clipping so you can ignore adding that extra detail.
I’m currently seeing the map like this:
I want to display proper map labels, similar to the view below. Currently, the vector layer only adds the Indian states using a topojson file (india_states.json) that contains just the state boundaries — no labels or additional data.
Here’s the relevant code from my MapComponent:
import React, { useEffect, useRef } from 'react';
import 'ol/ol.css';
import 'ol-ext/dist/ol-ext.css';
const MapComponent = ({ onLocationSelect }) => {
const mapRef = useRef(null);
const magnifyRef = useRef(null);
useEffect(() => {
// Dynamically import OpenLayers modules to ensure they only run in browser environment
import('ol/Map').then(({ default: Map }) => {
import('ol/View').then(({ default: View }) => {
import('ol/layer/Tile').then(({ default: TileLayer }) => {
import('ol/source/OSM').then(({ default: OSM }) => {
import('ol/proj').then(({ fromLonLat, toLonLat }) => {
import('ol-ext/control/LayerSwitcher').then(
({ default: LayerSwitcher }) => {
import('ol-ext/overlay/Magnify').then(
({ default: Magnify }) => {
import('ol/layer/Vector').then(
({ default: VectorLayer }) => {
import('ol/source/Vector').then(
({ default: VectorSource }) => {
import('ol/format/GeoJSON').then(
({ default: GeoJSON }) => {
import('ol/style/Style').then(
({ default: Style }) => {
import('ol/style/Stroke').then(
({ default: Stroke }) => {
import('ol/style/Fill').then(
({ default: Fill }) => {
import(
'ol/format/TopoJSON'
).then(
({ default: TopoJSON }) => {
import('ol/render').then(
({ getVectorContext }) => {
// OSM Base layer
const osmLayer =
new TileLayer({
source: new OSM(),
});
// India States Vector Layer (used for clipping)
const indiaStatesSource =
new VectorSource({
url: '/topoJsons/india_states.json',
format:
new TopoJSON(),
});
const indiaStatesLayer =
new VectorLayer({
source:
indiaStatesSource,
style: new Style({
stroke: new Stroke(
{
color:
'#3388ff',
width: 2,
}
),
fill: new Fill({
color:
'rgba(51, 136, 255, 0.1)',
}),
}),
});
// Set extent for clipping when features are loaded
indiaStatesSource.on(
'addfeature',
function () {
const extent =
indiaStatesSource.getExtent();
osmLayer.setExtent(
extent
);
}
);
// Clipping style for rendering
const clipStyle =
new Style({
fill: new Fill({
color: 'black',
}),
});
// Add post-render event for clipping
osmLayer.on(
'postrender',
function (e) {
const vectorContext =
getVectorContext(
e
);
e.context.globalCompositeOperation =
'destination-in';
indiaStatesSource.forEachFeature(
function (
feature
) {
vectorContext.drawFeature(
feature,
clipStyle
);
}
);
e.context.globalCompositeOperation =
'source-over';
}
);
// India Districts Vector Layer
const indiaDistrictSource =
new VectorSource({
url: '/topoJsons/india_districts.json',
format:
new TopoJSON(),
});
const indiaDistrictsLayer =
new VectorLayer({
source:
indiaDistrictSource,
style: new Style({
stroke: new Stroke(
{
color:
'#FF0000',
width: 1,
}
),
fill: new Fill({
color:
'rgba(255, 0, 0, 0.1)',
}),
}),
visible: false,
});
indiaDistrictsLayer.set(
'title',
'India Districts'
);
// India Subdistricts Vector Layer
const indiaSubdistrictSource =
new VectorSource({
url: '/topoJsons/india_subdistricts.json',
format:
new TopoJSON(),
});
const indiaSubdistrictsLayer =
new VectorLayer({
source:
indiaSubdistrictSource,
style: new Style({
stroke: new Stroke(
{
color:
'#FFFF00',
width: 1,
}
),
fill: new Fill({
color:
'rgba(255, 255, 0, 0.1)',
}),
}),
visible: false,
});
indiaSubdistrictsLayer.set(
'title',
'India Subdistricts'
);
const map = new Map({
target: mapRef.current,
layers: [
osmLayer,
indiaStatesLayer,
indiaDistrictsLayer,
indiaSubdistrictsLayer,
],
view: new View({
center: fromLonLat([
78.9629, 20.5937,
]), // Center of India
zoom: 4.5,
// Remove extent restriction to show full India
minZoom: 4,
maxZoom: 10,
}),
});
const switcher =
new LayerSwitcher();
map.addControl(switcher);
// Magnify overlay with OSM source
const magnifySource =
new OSM();
const magnifyLayer =
new TileLayer({
source:
magnifySource,
});
const magnify =
new Magnify({
layers: [
magnifyLayer,
],
radius: 120,
zoomOffset: 2,
rotateWithView:
false,
stabilized: true,
});
map.addOverlay(magnify);
magnifyRef.current =
magnify;
map.on(
'pointermove',
(evt) => {
magnify.setPosition(
evt.coordinate
);
}
);
map.on(
'click',
(evt) => {
const lonLat =
toLonLat(
evt.coordinate
);
// Pass the click coordinates to the parent component
onLocationSelect({
lat: lonLat[1],
lon: lonLat[0],
});
}
);
setTimeout(() => {
map.updateSize();
}, 100);
}
);
}
);
}
);
}
);
}
);
}
);
}
);
}
);
}
);
}
);
});
});
});
});
});
return () => {
// Cleanup function to prevent memory leaks
if (mapRef.current) {
mapRef.current.innerHTML = '';
}
};
}, [onLocationSelect]);
return (
<div
ref={mapRef}
className="map-container"
style={{
width: '100%',
margin: 'auto',
height: '750px',
border: '1px solid #ccc',
}}
/>
);
};
export default MapComponent;
I want my map to look more like this, with detailed base map labels visible:
Is there a way to retain OSM (or a similar base map) with proper labeling while still applying the clipping/masking based on my TopoJSON boundaries?
Any help would be appreciated!