Compare commits

...
Sign in to create a new pull request.

26 commits

Author SHA1 Message Date
j
29df9f1760 fix format 2025-08-13 09:34:55 +02:00
j
ef24247a36 more nominatim tweaks 2025-08-12 12:32:34 +02:00
j
529277eb12 toggle style 2025-08-11 19:55:08 +02:00
j
95b3a4be6e fix controls height 2025-08-11 19:52:24 +02:00
j
3c482989d1 fix pan controls 2025-08-11 19:36:59 +02:00
j
e274d88c2b faster lookup, start to map addresstype to type 2025-08-11 18:47:25 +02:00
j
bbc8debf46 Merge branch 'maplibre-big-bbox' into maplibre 2025-08-11 18:29:59 +02:00
j
f5d15ce75f use Ox.api async mode and clear cache on change 2025-08-11 18:27:50 +02:00
j
c4591c3a0e don't clone places if its a function 2025-08-11 18:26:33 +02:00
Sanjay Bhangar
905258c962 pick nominatim result as per zoom level, sending current zoom as part of nominatim request 2025-08-11 18:14:51 +02:00
j
d737817528 fix place id generation in example 2025-08-11 17:19:18 +02:00
Sanjay Bhangar
a0b1e0eab4 Here's what we've accomplished to fix MapLibre GL rectangle editing:
Core Issues Fixed:

  1. MapLibre GL Event Compatibility: Fixed drag event handlers in MapMarker.js and MapRectangleMarker.js to use marker.getLngLat() instead of accessing e.lngLat properties that don't exist in MapLibre GL.
  2. Rectangle Handle Visibility: Replaced Ox.MapMarkerImage() with proper DOM elements for corner handles since MapLibre GL expects DOM elements, not image objects.
  3. Handle Positioning: Limited corner markers to only the 4 corners (ne, nw, se, sw) instead of all 8 positions (including edges).
  4. Visual Rectangle Updates: Added that.rectangle.setBounds() call in MapRectangle update method so the visual rectangle updates when handles are dragged.
  5. Click Event Conflicts: Fixed map click handler to detect rectangle clicks and avoid deselecting rectangles when clicked for editing.
  6. Place Editable Property: Added editable: true as default in MapPlace constructor.

  Files Modified:

  - MapPlace.js: Added editable: true default
  - MapRectangle.js:
    - Limited markers to 4 corners only
    - Added rectangle bounds update in update() method
  - MapRectangleMarker.js:
    - Created proper DOM elements for handles
    - Fixed MapLibre GL drag event handling
    - Improved event cleanup
  - MapMarker.js: Fixed MapLibre GL drag event handling
  - MapMarkerImage.js: Fixed rectangle marker color (kept for backward compatibility)
  - Map.js: Added rectangle click detection to prevent conflicts

  Result:

  Rectangle editing now works completely:
  - Click rectangle → shows 4 corner handles
  - Drag handles → resizes rectangle in real-time
  - No console errors
  - Proper event handling
2025-08-09 14:15:54 +02:00
j
80d3919b46 one layer per rectangle to redouce events 2025-08-06 23:31:07 +02:00
j
1bac02d24f fixup 2025-08-06 23:18:21 +02:00
j
56123e4575 map_editor example should use async code 2025-08-06 23:02:19 +02:00
j
bc174c49dd support updating MapEditor places 2025-08-06 23:01:31 +02:00
j
f3e0632f0e more fixes 2025-08-06 21:50:00 +02:00
j
bcade5f2d7 add map editor example 2025-08-06 21:49:45 +02:00
j
8dbfcc0ece fix raster subdomains 2025-08-06 19:30:08 +02:00
j
6a59ff5193 marker size 2025-08-06 19:29:16 +02:00
j
e917bce40d correct cross the 180th meridian (antimeridian) handling 2025-08-06 19:10:04 +02:00
j
66fca1fc02 dev mode 2025-08-06 19:03:10 +02:00
j
8cebad9fb4 migrate to maplibre-gl for maps 2025-08-06 19:03:10 +02:00
j
f3b8025e8e add maplibre-gl-geocoder 2025-08-06 14:02:53 +02:00
j
25cd3f6bb1 include maplibre-gl files in build 2025-08-05 19:09:19 +02:00
j
76b9517a78 add maplibre-gl.js 2025-08-05 18:50:09 +02:00
16 changed files with 1180 additions and 274 deletions

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<title>World Map with Countries</title>
<meta http-equiv="Keywords" content="Lists"/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<link rel="shortcut icon" type="image/png" href="../../../source/UI/themes/oxlight/png/icon16.png"/>
<script type="text/javascript" src="../../../dev/Ox.js"></script>
<script type="text/javascript" src="js/example.js"></script>
<script>window.addEventListener('message', function(e) { e.origin == window.location.origin && eval('(' + e.data + ')'); });</script>
</head>
<body></body>
</html>

View file

@ -0,0 +1,88 @@
/*
In this example, we use Ox.MapEditor
*/
'use strict';
Ox.load(['UI', 'Geo'], function() {
var $storage = Ox.localStorage("map_editor")
var places = $storage('places') || []
var placesAPI = Ox.api(places, {
geo: true,
sort: '-area',
cache: true,
})
var $map = Ox.MapEditor({
addPlace: function(place, callback) {
place = Ox.clone(place)
place.id = Ox.encodeBase26((places.length ? Ox.max(places.map(p => Ox.decodeBase26(p.id))) : 0) + 1)
place.editable = true;
console.log("addPlace", place.id, place)
places.push(place)
$storage("places", places)
placesAPI.update(places)
setTimeout(() => {
Ox.Request.clearCache();
callback({
status: {
code: 200
},
data: place
})
}, 200)
},
editPlace: function(place, callback) {
place = Ox.clone(place)
places.forEach(p => {
if (p.id == place.id) {
Object.assign(p, place);
place = p
}
})
$storage("places", places)
placesAPI.update(places)
setTimeout(() => {
Ox.Request.clearCache();
callback({
status: {
code: 200
},
data: place
})
}, 200)
},
getMatches: function(names, callback) {
console.log("getMatches", names)
callback(23);
},
hasMatches: true, // FIXME: getMatches is enough
height: 800,
mode: 'add', // 'define',
names: null,
places: placesAPI,
removePlace: function(place, callback) {
console.log("removePlace", place.id, places)
places = places.filter(p => { return p.id != place.id })
console.log("new places", places)
$storage("places", places)
placesAPI.update(places)
setTimeout(() => {
Ox.Request.clearCache();
callback({
status: {
code: 200
},
})
}, 200)
},
selected: '',
showControls: false,
showLabels: false,
showTypes: true,
width: 600
})
.appendTo(Ox.$body);
Ox.$window.bind({resize: $map.resizeMap});
});

View file

@ -5,9 +5,9 @@
<meta http-equiv="Keywords" content="Lists"/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<link rel="shortcut icon" type="image/png" href="../../../source/UI/themes/oxlight/png/icon16.png"/>
<script type="text/javascript" src="../../../min/Ox.js"></script>
<script type="text/javascript" src="../../../dev/Ox.js"></script>
<script type="text/javascript" src="js/example.js"></script>
<script>window.addEventListener('message', function(e) { e.origin == window.location.origin && eval('(' + e.data + ')'); });</script>
</head>
<body></body>
</html>
</html>

View file

@ -1800,6 +1800,7 @@ Maps
.OxMap .OxLabel.OxMapControl.OxMapScale {
right: 4px;
bottom: 19px;
line-height: 1;
}
.OxMap .OxPlaceControl.OxPlaceFlag {
@ -1821,6 +1822,7 @@ Maps
top: 4px;
width: 136px;
text-overflow: ellipsis;
line-height: 1;
}
.OxMap .OxPlaceControl.OxPlaceDeselectButton {
right: 4px;
@ -2969,3 +2971,20 @@ Miscellaneous
.OxTooltip > div {
font-size: 9px;
}
/*
================================================================================
MapLibre GL cleanups
================================================================================
*/
.maplibregl-popup-anchor-bottom .maplibregl-popup-tip {
display: none !important;
}
.maplibregl-popup-content {
padding: 0px !important;
border-radius: 5px;
top: 50px;
left: -4px;
}

View file

@ -99,6 +99,7 @@ Ox.Map = function(options, self) {
return place.name || '<span class="OxLight">Unnamed</span>';
},
maxMarkers: 100,
nominatim: 'https://nominatim.openstreetmap.org',
places: null,
selected: '',
showControls: false,
@ -106,6 +107,7 @@ Ox.Map = function(options, self) {
showStatusbar: false,
showToolbar: false,
showZoombar: false,
style: 'https://tiles.openfreemap.org/styles/liberty',
zoomOnlyWhenFocused: false
// fixme: width, height
})
@ -143,7 +145,7 @@ Ox.Map = function(options, self) {
self.map.fitBounds(mapBounds);
} else {
self.map.setZoom(self.minZoom);
self.map.setCenter(new google.maps.LatLng(0, 0));
self.map.setCenter(new maplibregl.LngLat(0, 0));
}
// fixme: the following is just a guess
self.boundsChanged = true;
@ -509,23 +511,27 @@ Ox.Map = function(options, self) {
$placeControl.css({opacity: 0}).hide();
});
if (window.google) {
if (window.maplibregl) {
// timeout needed so that the map is in the DOM
setTimeout(initMap);
} else if (window.googleCallback) {
(function interval() {
isLoaded() ? initMap() : setTimeout(interval, 100);
}());
} else {
window.googleCallback = function() {
delete window.googleCallback;
initMap();
};
$.getScript(
document.location.protocol
+ '//maps.google.com/maps/api/js?callback=googleCallback&sensor=false'
+ (Ox.Map.GoogleApiKey ? '&key=' + Ox.Map.GoogleApiKey : '')
);
Ox.getStylesheet([
Ox.PATH + 'UI/maplibre-gl/maplibre-gl.css',
Ox.PATH + 'UI/maplibre-gl/maplibre-gl-geocoder.css'
], () => {})
$.getScript([
Ox.PATH + 'UI/maplibre-gl/maplibre-gl.js',
Ox.PATH + 'UI/maplibre-gl/maplibre-gl-geocoder.min.js',
], initMap)
}
function equalBonds(a, b) {
return (
a._sw.lat == b._sw.lat &&
a._sw.lng == b._sw.lng &&
a._ne.lat == b._ne.lat &&
a._ne.lng == b._ne.lng
)
}
function addPlaceToMap(place) {
@ -535,11 +541,11 @@ Ox.Map = function(options, self) {
if (!place) {
var bounds = self.map.getBounds(),
center = self.map.getCenter(),
southwest = new google.maps.LatLngBounds(
bounds.getSouthWest(), center
southwest = new maplibregl.LngLatBounds(
bounds._sw, center
).getCenter(),
northeast = new google.maps.LatLngBounds(
center, bounds.getNorthEast()
northeast = new maplibregl.LngLatBounds(
center, bounds._ne
).getCenter(),
place = new Ox.MapPlace({
alternativeNames: [],
@ -550,14 +556,14 @@ Ox.Map = function(options, self) {
map: that,
name: '',
type: 'feature',
south: southwest.lat(),
west: southwest.lng(),
north: northeast.lat(),
east: northeast.lng()
south: southwest.lat,
west: southwest.lng,
north: northeast.lat,
east: northeast.lng
});
}
Ox.forEach(self.places, function(p, i) {
if (place.bounds.equals(p.bounds)) {
if (equalBonds(place.bounds, p.bounds)) {
place = p;
exists = true;
return false; // break
@ -589,17 +595,23 @@ Ox.Map = function(options, self) {
self.boundsChanged = true;
}
function toSpan(bounds) {
return {
lat: bounds._ne.lat - bounds._sw.lat,
lng: bounds._ne.lng - bounds._sw.lng,
}
}
function canContain(outerBounds, innerBounds) {
// checks if outerBounds _can_ contain innerBounds
var outerSpan = outerBounds.toSpan(),
innerSpan = innerBounds.toSpan();
return outerSpan.lat() > innerSpan.lat() &&
outerSpan.lng() > innerSpan.lng();
var outerSpan = toSpan(outerBounds),
innerSpan = toSpan(innerBounds);
return outerSpan.lat > innerSpan.lat &&
outerSpan.lng > innerSpan.lng;
}
function centerChanged() {
var tooltip = $('.OxMapMarkerTooltip');
tooltip.length && Ox.$elements[$(tooltip[0]).data('oxid')].hide();
that.tooltip.remove()
self.center = self.map.getCenter();
self.centerChanged = true;
}
@ -609,9 +621,21 @@ Ox.Map = function(options, self) {
}
function clickMap(event) {
// Check if click hit any rectangle fill layers (which have rectangle click handlers)
var features = self.map.queryRenderedFeatures(event.point, {
layers: self.map.getStyle().layers
.filter(layer => layer.id.includes('-fill'))
.map(layer => layer.id)
});
// If we clicked on a rectangle, don't process map click
if (features.length > 0) {
return;
}
var place = getSelectedPlace();
if (self.options.clickable/* && !editing()*/) {
getPlaceByLatLng(event.latLng, self.map.getBounds(), function(place) {
getPlaceByLatLng(event.lngLat, self.map.getBounds(), function(place) {
if (place) {
addPlaceToMap(place);
//selectPlace(place.id);
@ -656,7 +680,7 @@ Ox.Map = function(options, self) {
function crossesDateline() {
var bounds = self.map.getBounds();
return bounds.getSouthWest().lng() > bounds.getNorthEast().lng();
return bounds._sw.lng > bounds._ne.lng;
}
function editing() {
@ -681,9 +705,9 @@ Ox.Map = function(options, self) {
// get initial map bounds
self.options.places({}, function(result) {
var area = result.data.area;
callback(new google.maps.LatLngBounds(
new google.maps.LatLng(area.south, area.west),
new google.maps.LatLng(area.north, area.east)
callback(new maplibregl.LngLatBounds(
new maplibregl.LngLat(area.west, area.south),
new maplibregl.LngLat(area.east, area.north)
));
});
}
@ -695,24 +719,28 @@ Ox.Map = function(options, self) {
- self.options.showZoombar * 16;
}
function getMapType() {
return self.options.showLabels ? 'HYBRID' : 'SATELLITE'
}
function getMaxZoom(point, callback) {
if (arguments.length == 1) {
callback = point;
point = self.map.getCenter();
}
// fixme, why is getMaxZoom off by one?
let maxZoom = self.map.getMaxZoom()
setTimeout(() => {
callback(maxZoom)
})
/*
self.maxZoomService.getMaxZoomAtLatLng(point, function(data) {
callback(data.status == 'OK' ? data.zoom : null);
});
*/
}
function getMetersPerPixel() {
// m/px = m/deg * deg/px
var degreesPerPixel = 360 / (self.tileSize * Math.pow(2, self.map.getZoom()));
return Ox.getMetersPerDegree(self.map.getCenter().lat()) * degreesPerPixel;
const degreesPerPixel = 360 / (self.tileSize * Math.pow(2, self.map.getZoom()));
const center = self.map.getCenter();
return Ox.getMetersPerDegree(center.lat) * degreesPerPixel;
}
function getMinZoom() {
@ -734,61 +762,40 @@ Ox.Map = function(options, self) {
: Ox.getObjectById(self.places, id);
}
function getPlaceByLatLng(latlng, bounds, callback) {
// gets the largest place at latlng that would fit in bounds
async function getPlaceByLatLng(latlng, bounds, callback) {
// gets the place at latlng appropriate for current zoom level
var callback = arguments.length == 3 ? callback : bounds,
bounds = arguments.length == 3 ? bounds : null;
self.$loadingIcon && self.$loadingIcon.start();
self.geocoder.geocode({
latLng: latlng
}, function(results, status) {
self.$loadingIcon && self.$loadingIcon.stop();
if (status == google.maps.GeocoderStatus.OK) {
if (bounds) {
Ox.forEach(results.reverse(), function(result, i) {
if (
i == results.length - 1 ||
canContain(bounds, result.geometry.bounds || result.geometry.viewport)
) {
callback(new Ox.MapPlace(parseGeodata(results[i])));
return false; // break
}
});
} else {
callback(new Ox.MapPlace(parseGeodata(results[0])));
}
}
if (
status == google.maps.GeocoderStatus.OK ||
status == google.maps.GeocoderStatus.ZERO_RESULTS
) {
triggerGeocodeEvent({
latLng: latlng,
results: results
});
} else {
Ox.Log('Map', 'geocode failed:', status);
callback(null);
}
});
var results = await reverseGeocode(latlng);
self.$loadingIcon && self.$loadingIcon.stop();
if (results.features.length) {
// We only have one result based on current zoom level
var feature = results.features[0];
callback(new Ox.MapPlace(parseGeodata(feature)));
triggerGeocodeEvent({
latLng: latlng,
results: results.features
});
} else {
Ox.Log('Map', 'geocode failed: no results');
callback(null);
}
}
function getPlaceByName(name, callback) {
self.$loadingIcon && self.$loadingIcon.start();
self.geocoder.geocode({
address: name
}, function(results, status) {
forwardGeocode({
query: name
}).then(function(results) {
self.$loadingIcon && self.$loadingIcon.stop();
if (status == google.maps.GeocoderStatus.OK) {
callback(new Ox.MapPlace(parseGeodata(results[0])));
}
if (
status == google.maps.GeocoderStatus.OK
&& status != google.maps.GeocoderStatus.ZERO_RESULTS
) {
if (results.features.length) {
callback(new Ox.MapPlace(parseGeodata(results.features[0])));
triggerGeocodeEvent({
address: name,
results: results
results: results.features
});
} else {
Ox.Log('Map', 'geocode failed:', status);
@ -831,36 +838,192 @@ Ox.Map = function(options, self) {
: null;
}
function bbox2bounds(bbox) {
return new maplibregl.LngLatBounds(
new maplibregl.LngLat(bbox[0], bbox[1]),
new maplibregl.LngLat(bbox[2], bbox[3])
)
}
function converNominatimFeature(feature) {
const center = [
feature.bbox[0] +
(feature.bbox[2] - feature.bbox[0]) / 2,
feature.bbox[1] +
(feature.bbox[3] - feature.bbox[1]) / 2
];
const polygon = {
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: [
[feature.bbox[0], feature.bbox[1]],
[feature.bbox[1], feature.bbox[2]],
[feature.bbox[2], feature.bbox[3]],
[feature.bbox[0], feature.bbox[3]],
]
},
bounds: bbox2bounds(feature.bbox),
place_name: feature.properties.display_name,
properties: feature.properties,
text: feature.properties.display_name,
address_components: [
{
long_name: feature.properties.display_name,
short_name: feature.properties.display_name,
types: ['place']
}
],
place_type: ['place'],
};
return polygon
}
async function reverseGeocode(config) {
const features = [];
// Use current map zoom level directly as Nominatim zoom level
const currentMapZoom = Math.round(self.map.getZoom());
// FIXME: map to nominatim zoom levels:
// https://nominatim.org/release-docs/develop/api/Reverse/
// zoom address detail
// 3 country
// 5 state
// 8 county
// 10 city
// 12 town / borough
// 13 village / suburb
// 14 neighbourhood
// 15 any settlement
// 16 major streets
// 17 major and minor streets
// 18 building
//
console.log('Reverse geocoding: using map zoom =', currentMapZoom, 'for Nominatim zoom');
let request = `${self.options.nominatim}/reverse?lat=${
config.lat
}&lon=${
config.lng
}&format=geojson&polygon_geojson=0&addressdetails=1&extratags=1&accept-language=en`;
request += `&zoom=${currentMapZoom}`
try {
const response = await fetch(request);
const geojson = await response.json();
if (geojson.features && geojson.features.length > 0) {
const feature = converNominatimFeature(geojson.features[0]);
feature.zoom = currentMapZoom;
features.push(feature);
}
} catch (e) {
console.error(`Failed to reverseGeocode with error: ${e}`);
}
return {
features
};
}
async function forwardGeocode(config) {
const features = [];
const request = `${self.options.nominatim}/search?q=${
config.query
}&format=geojson&polygon_geojson=0&addressdetails=1&accept-language=en`;
try {
const response = await fetch(request);
const geojson = await response.json();
for (const feature of geojson.features) {
features.push(converNominatimFeature(feature));
}
} catch (e) {
console.error(`Failed to forwardGeocode with error: ${e}`);
}
return {
features
};
}
function initMap() {
getMapBounds(function(mapBounds) {
//Ox.Log('Map', 'init', mapBounds.getSouthWest(), mapBounds.getNorthEast(), mapBounds.getCenter())
//Ox.Log('Map', 'init', mapBounds._sw, mapBounds._ne, mapBounds.getCenter())
self.elevationService = new google.maps.ElevationService();
self.geocoder = new google.maps.Geocoder();
self.maxZoomService = new google.maps.MaxZoomService();
//self.elevationService = new google.maps.ElevationService();
//self.maxZoomService = new google.maps.MaxZoomService();
//
self.center = mapBounds ? mapBounds.getCenter() : new google.maps.LatLng(0, 0);
self.center = mapBounds ? mapBounds.getCenter() : new maplibregl.LngLat(0, 0);
self.zoom = self.minZoom;
that.map = self.map = new google.maps.Map(self.$map[0], {
self.sateliteStyle = {
'version': 8,
'sources': {
'raster-tiles': {
'type': 'raster',
'tiles': [
'https://mt0.google.com/vt/lyrs=s&x={x}&y={y}&z={z}',
'https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}',
'https://mt2.google.com/vt/lyrs=s&x={x}&y={y}&z={z}',
'https://mt3.google.com/vt/lyrs=s&x={x}&y={y}&z={z}',
],
'tileSize': 256,
'attribution':
'FIXME',
},
},
'layers': [
{
'id': 'simple-tiles',
'type': 'raster',
'source': 'raster-tiles',
'roundZoom': true,
'minzoom': 0,
'maxzoom': 22
},
]
};
window.map = that.map = self.map = new maplibregl.Map({
container: self.$map[0],
center: self.center,
disableDefaultUI: true,
disableDoubleClickZoom: true,
mapTypeId: google.maps.MapTypeId[getMapType()],
noClear: true,
scrollwheel: !self.options.zoomOnlyWhenFocused,
style: self.options.showLabels ? self.options.style : self.sateliteStyle,
//noClear: true,
//scrollwheel: !self.options.zoomOnlyWhenFocused,
zoom: self.zoom
})
self.geocoder = new MaplibreGeocoder({
forwardGeocode: forwardGeocode,
reverseGeocode: reverseGeocode,
})
/*
map.addControl(self.geocoder, {
maplibregl
});
*/
that.map.on('click', clickMap)
that.map.on('zoom', zoomChanged)
that.map.on('idle', mapChanged)
that.map.on('moveend', boundsChanged)
that.map.on('dragend', boundsChanged)
that.map.on('zoomend', boundsChanged)
/*
that.map.on('resize', () => {
that.resizeMap()
})
*/
/*
google.maps.event.addListener(self.map, 'bounds_changed', boundsChanged);
google.maps.event.addListener(self.map, 'center_changed', centerChanged);
google.maps.event.addListener(self.map, 'click', clickMap);
google.maps.event.addListener(self.map, 'idle', mapChanged);
google.maps.event.addListener(self.map, 'zoom_changed', zoomChanged);
google.maps.event.trigger(self.map, 'resize');
*/
// needed to get mouse x/y coordinates on marker mouseover,
// see http://code.google.com/p/gmaps-api-issues/issues/detail?id=2342
/*
that.overlayView = new google.maps.OverlayView();
that.overlayView.setMap(self.map);
that.overlayView.draw = function () {
@ -870,52 +1033,55 @@ Ox.Map = function(options, self) {
}
}
that.overlayView.draw();
*/
Ox.forEach(self.$controls, function($control) {
$control.appendTo(self.$map);
});
Ox.forEach(self.$placeControls, function($placeControl) {
$placeControl.appendTo(self.$map);
});
that.map.on('load', () => {
Ox.forEach(self.$controls, function($control) {
$control.appendTo(self.$map);
});
Ox.forEach(self.$placeControls, function($placeControl) {
$placeControl.appendTo(self.$map);
});
if (self.options.find) {
self.$findInput
.value(self.options.find)
.triggerEvent('submit', {value: self.options.find});
} else {
if (self.options.selected) {
selectPlace(self.options.selected, true);
}
if (mapBounds) {
if (isEmpty(mapBounds)) {
if (self.options.find) {
self.$findInput
.value(self.options.find)
.triggerEvent('submit', {value: self.options.find});
} else {
if (self.options.selected) {
selectPlace(self.options.selected, true);
}
if (mapBounds) {
if (isEmpty(mapBounds)) {
self.map.setZoom(self.minZoom);
} else {
self.map.fitBounds(mapBounds);
}
}
if (self.map.getZoom() < self.minZoom) {
self.map.setZoom(self.minZoom);
} else {
self.map.fitBounds(mapBounds);
}
}
if (self.map.getZoom() < self.minZoom) {
self.map.setZoom(self.minZoom);
}
}
updateFormElements();
updateFormElements();
self.loaded = true;
that.triggerEvent('load');
that.resizeMap()
self.loaded = true;
self.boundsChanged = true;
that.triggerEvent('load');
})
});
}
function isEmpty(bounds) {
// Google's bounds.isEmpty() is not reliable
var southWest = bounds.getSouthWest(),
northEast = bounds.getNorthEast();
return southWest.lat() == northEast.lat()
&& southWest.lng() == northEast.lng();
return bounds._sw.lat == bounds._ne.lat
&& bounds._sw.lng == bounds._ne.lng;
}
function isLoaded() {
return window.google && window.google.maps && window.google.maps.LatLng;
return window.maplibregl && window.maplibregl.maps && window.maplibregl.LngLat;
}
function mapChanged() {
@ -927,12 +1093,11 @@ Ox.Map = function(options, self) {
self.boundsChanged = false;
return
}
var southWest = bounds.getSouthWest(),
northEast = bounds.getNorthEast(),
south = southWest.lat(),
west = southWest.lng(),
north = northEast.lat(),
east = northEast.lng();
var south = bounds._sw.lat,
west = bounds._sw.lng,
north = bounds._ne.lat,
east = bounds._ne.lng;
self.options.places({
keys: self.placeKeys,
query: {
@ -999,7 +1164,8 @@ Ox.Map = function(options, self) {
}
function pan(x, y) {
self.map.panBy(x * self.$map.width() / 2, y * self.$map.height() / 2);
console.log(x, y, self.$map.width())
self.map.panBy([x * self.$map.width() / 2, y * self.$map.height() / 2]);
};
function panToPlace() {
@ -1015,22 +1181,24 @@ Ox.Map = function(options, self) {
}
function parseGeodata(data) {
var bounds = data.geometry.bounds || data.geometry.viewport,
northEast = bounds.getNorthEast(),
southWest = bounds.getSouthWest(),
console.log("parseGeodata", data)
// FIXME: data is geojson Feature with Polygon geometry now
var bounds = data.bounds,
northEast = bounds._ne,
southWest = bounds._sw,
place = {
alternativeNames: [],
components: data.address_components,
countryCode: getCountryCode(data.address_components),
east: northEast.lng(),
east: northEast.lng,
editable: self.options.editable,
fullGeoname: getFullGeoname(data.address_components),
id: '_' + Ox.encodeBase32(Ox.uid()),
map: that,
north: northEast.lat(),
south: southWest.lat(),
type: getType(data.address_components[0].types),
west: southWest.lng()
north: northEast.lat,
south: southWest.lat,
type: getType(data.address_components[0].types, data),
west: southWest.lng
};
place.geoname = data.formatted_address || place.fullGeoname;
place.name = (place.geoname || place.fullGeoname).split(', ')[0];
@ -1062,19 +1230,34 @@ Ox.Map = function(options, self) {
) ? name : null;
}).join(', ');
}
function getType(types) {
function getType(types, data) {
// see https://developers.google.com/maps/documentation/javascript/geocoding#GeocodingAddressTypes
types.unshift(data.properties.addresstype)
console.log(types)
var strings = {
'country': ['country'],
'region': ['administrative_area', 'colloquial_area'],
'city': ['locality'],
'borough': ['neighborhood', 'postal_code', 'sublocality'],
'region': [
'administrative_area', 'colloquial_area',
'state', 'county', 'region'
],
'city': [
'locality', 'city', 'town', 'village'
],
'borough': [
'neighborhood', 'postal_code', 'sublocality', 'suburb', 'borough',
'neighbourhood',
],
'street': [
'intersection', 'route',
'street_address', 'street_number'
'street_address', 'street_number',
'road',
],
'building': [
'airport', 'floor', 'premise', 'room', 'subpremise'
'airport', 'floor', 'premise', 'room', 'subpremise',
'building',
],
'feature': ['natural_feature', 'park']
},
@ -1326,7 +1509,7 @@ Ox.Map = function(options, self) {
function toggleLabels() {
self.options.showLabels = !self.options.showLabels;
self.map.setMapTypeId(google.maps.MapTypeId[getMapType()]);
self.map.setStyle(self.options.showLabels ? self.options.style : self.sateliteStyle)
that.triggerEvent('togglelabels', {
visible: self.options.showLabels
});
@ -1336,10 +1519,12 @@ Ox.Map = function(options, self) {
// someone may want to cache google geocode data, so we fire an event.
// google puts functions like lat or lng on the objects' prototypes,
// so we create properly named properties, for json encoding
console.log(data)
/*
if (data.latLng) {
data.latLng = {
lat: data.latLng.lat(),
lng: data.latLng.lng()
lat: data.latLng.lat,
lng: data.latLng.lng
}
}
data.results.forEach(function(result) {
@ -1347,23 +1532,24 @@ Ox.Map = function(options, self) {
if (result.geometry[key]) {
result.geometry[key] = {
northEast: {
lat: result.geometry[key].getNorthEast().lat(),
lng: result.geometry[key].getNorthEast().lng()
lat: result.geometry[key]._ne.lat,
lng: result.geometry[key]._ne.lng
},
southWest: {
lat: result.geometry[key].getSouthWest().lat(),
lng: result.geometry[key].getSouthWest().lng()
lat: result.geometry[key]._sw.lat,
lng: result.geometry[key]._sw.lng
}
}
}
});
if (result.geometry.location) {
result.geometry.location = {
lat: result.geometry.location.lat(),
lng: result.geometry.location.lng()
lat: result.geometry.location.lat,
lng: result.geometry.location.lng
}
}
});
*/
that.triggerEvent('geocode', data);
}
@ -1402,7 +1588,7 @@ Ox.Map = function(options, self) {
}
function zoomChanged() {
var zoom = self.map.getZoom();
var zoom = parseInt(self.map.getZoom());
if (zoom < self.minZoom) {
self.map.setZoom(self.minZoom);
} else if (self.maxZoom && zoom > self.maxZoom) {
@ -1443,8 +1629,7 @@ Ox.Map = function(options, self) {
lng <n> Longitude
@*/
that.getCenter = function() {
var center = self.map.getCenter();
return {lat: center.lat(), lng: center.lng()};
return self.map.getCenter();
};
/*@
@ -1544,7 +1729,7 @@ Ox.Map = function(options, self) {
});
updateFormElements();
Ox.Log('Map', 'triggering google maps resize event, height', self.options.height)
google.maps.event.trigger(self.map, 'resize');
self.map.triggerRepaint()
// self.map.setCenter(center);
}
return that;
@ -1558,7 +1743,7 @@ Ox.Map = function(options, self) {
lng <n> Longitude
@*/
that.setCenter = function(center) {
self.map.setCenter(new google.maps.LatLng(center.lat, center.lng));
self.map.setCenter(new maplibregl.LngLat(center.lng, center.lat));
return that;
};

View file

@ -46,6 +46,15 @@ Ox.MapEditor = function(options, self) {
self.$list.size();
self.$map.resizeMap();
},
places: function() {
self.isAsync = Ox.isFunction(self.options.places);
self.$list.options({
items: self.isAsync ? self.options.places : Ox.clone(self.options.places)
})
self.$map.options({
places: self.options.places
})
},
selected: function() {
self.$list.options({selected: self.options.selected});
},
@ -328,7 +337,7 @@ Ox.MapEditor = function(options, self) {
columns: self.columns,
columnsRemovable: true,
columnsVisible: true,
items: Ox.clone(self.options.places),
items: self.isAsync ? self.options.places : Ox.clone(self.options.places),
//items: self.options.places,
// area needed for icon, geoname needed for flag
keys: ['area', 'geoname', 'matches'],

View file

@ -42,16 +42,32 @@ Ox.MapMarker = function(options) {
Ox.forEach(options, function(val, key) {
that[key] = val;
});
that.marker = new google.maps.Marker({
setColor()
setSize()
const element = document.createElement('div')
element.style.border = '2px solid black'
element.style.borderRadius = '50px'
element.style.backgroundColor = '#' + Ox.toHex(that.color)
element.style.width = element.style.height = that.size + 'px'
that.marker = new maplibregl.Marker({
raiseOnDrag: false,
shape: {coords: [8, 8, 8], type: 'circle'}
element: element,
//shape: {coords: [8, 8, 8], type: 'circle'},
//title: that.place.name,
//zIndex: 1000
});
that.tooltip = new maplibregl.Popup({
closeButton: false,
closeOnClick: false,
className: 'tooltip'
});
that.tooltip.addClass
setOptions();
function click() {
function click(event) {
event.preventDefault()
event.stopPropagation()
var key = that.map.getKey(),
place, bounds, southWest, northEast;
if (!that.place.selected) {
@ -62,9 +78,9 @@ Ox.MapMarker = function(options) {
place = that.map.getSelectedPlace();
}
if (place) {
bounds = new google.maps.LatLngBounds(
new google.maps.LatLng(place.south, place.west),
new google.maps.LatLng(place.north, place.east)
bounds = new maplibregl.LngLatBounds(
new maplibregl.LngLatBounds(place.west, place.south),
new maplibregl.LngLatBounds(place.east, place.north)
);
bounds = bounds.union(that.place.bounds);
southWest = bounds.getSouthWest();
@ -79,10 +95,10 @@ Ox.MapMarker = function(options) {
map: that.map,
name: '',
type: 'feature',
south: southWest.lat(),
west: southWest.lng(),
north: northEast.lat(),
east: northEast.lng()
south: southWest.lat,
west: southWest.lng,
north: northEast.lat,
east: northEast.lng
}));
} else {
that.map.options({selected: that.place.id});
@ -105,13 +121,15 @@ Ox.MapMarker = function(options) {
}
function drag(e) {
// In MapLibre GL, get current position from marker directly
var lngLat = that.marker.getLngLat();
var northSouth = (that.place.north - that.place.south) / 2,
lat = Ox.limit(
e.latLng.lat(),
lngLat.lat,
Ox.MIN_LATITUDE + northSouth,
Ox.MAX_LATITUDE - northSouth
),
lng = e.latLng.lng(),
lng = lngLat.lng,
span = Math.min(
that.place.sizeEastWest * Ox.getDegreesPerMeter(lat) / 2, 179.99999999
),
@ -127,9 +145,7 @@ Ox.MapMarker = function(options) {
}
Ox.Log('Map', 'west', that.place.west, 'east', that.place.east, 'span', span);
that.place.update();
that.marker.setOptions({
position: that.place.center
});
that.marker.setLngLat(that.place.center)
that.place.rectangle.update();
}
@ -181,35 +197,30 @@ Ox.MapMarker = function(options) {
c.context.strokeStyle = 'rgb(' + border.join(', ') + ')';
c.context.arc(r, r, r - 1, 0, 360);
c.context.stroke();
callback(new google.maps.MarkerImage(
callback(new maplibregl.MarkerImage(
c.canvas.toDataURL(),
new google.maps.Size(options.size, options.size),
new google.maps.Point(0, 0),
new google.maps.Point(r, r)
new maplibregl.Size(options.size, options.size),
new maplibregl.Point(0, 0),
new maplibregl.Point(r, r)
));
}
}
function mouseover(e) {
var offset = that.map.offset(),
xy = that.map.overlayView.getProjection()
.fromLatLngToContainerPixel(e.latLng);
that.tooltip.show(
offset.left + Math.round(xy.x) - 4,
offset.top + Math.round(xy.y) + 20
);
that.tooltip.setLngLat(that.place.center).setHTML(
'<div style="display: flex; gap: 8px; margin: 2px 2px"><img src="' + Ox.getFlagByGeoname(that.place.geoname, 16)
+ '" style="border-radius: 4px;margin: auto"/>'
+ '<div style="font-size: 9px;">'
+ that.map.options('markerTooltip')(that.place) + '</div></div>'
).addTo(that.map.map);
}
function mouseout() {
that.tooltip.hide();
that.tooltip.remove();
}
function setOptions() {
// workaround to prevent marker from appearing twice
// after setting draggable from true to false (google maps bug)
var fix = that.marker.getDraggable() && !that.place.editing,
color = that.map.options('markerColor'),
size = that.map.options('markerSize');
function setColor() {
var color = that.map.options('markerColor');
//Ox.Log('Map', 'setOptions, that.map: ', that.map)
if (color == 'auto') {
that.color = typeColor[that.place.type] || typeColor['mapPlaceFeatureColor'];
@ -218,6 +229,10 @@ Ox.MapMarker = function(options) {
} else {
that.color = color(that.place);
}
}
function setSize() {
var size = that.map.options('markerSize');
if (size == 'auto') {
that.size = 8;
Ox.forEach(areaSize, function(size, area) {
@ -232,50 +247,57 @@ Ox.MapMarker = function(options) {
} else {
that.size = size(that.place);
}
}
function setOptions() {
// workaround to prevent marker from appearing twice
// after setting draggable from true to false (google maps bug)
var fix = false, // that.marker.getDraggable() && !that.place.editing,
size = that.map.options('markerSize');
//Ox.Log('Map', 'setOptions, that.map: ', that.map)
setColor()
setSize()
/* fixme, some of those can be set some not
that.marker.setOptions({
// fixme: cursor remains pointer
cursor: that.place.editing ? 'move' : 'pointer',
draggable: that.place.editing,
icon: Ox.MapMarkerImage({
element: Ox.MapMarkerImage({
color: that.color,
mode: that.place.editing ? 'editing' :
that.place.selected ? 'selected' : 'normal',
size: that.size,
type: that.place.id[0] == '_' ? 'result' : 'place'
}),
position: that.place.center
});
})
*/
//that.marker._color = that.color;
that.marker._element.style.cursor = that.place.editing ? 'move' : 'pointer';
that.marker._element.height = that.marker._element.width = that.size + 'px'
that.marker.setDraggable(that.place.editing);
that.marker.setLngLat(that.place.center);
if (fix) {
that.marker.setVisible(false);
setTimeout(function() {
that.marker.setVisible(true);
}, 0);
}
setTooltip();
}
function setTooltip() {
that.tooltip && that.tooltip.remove();
that.tooltip = Ox.Tooltip({
title: '<img src="'
+ Ox.getFlagByGeoname(that.place.geoname, 16)
+ '" style="float: left; width: 16px; height: 16px; margin: 1px 0 1px -1px; border-radius: 4px"/>'
+ '<div style="float: left; margin: 4px -1px 0 4px; font-size: 9px;">'
+ that.map.options('markerTooltip')(that.place) + '</div>'
})
.addClass('OxMapMarkerTooltip');
}
/*@
add <f> add to map
() -> <f> add to map, returns MapMarker
@*/
that.add = function() {
that.marker.setMap(that.map.map);
google.maps.event.addListener(that.marker, 'click', click);
google.maps.event.addListener(that.marker, 'dblclick', dblclick);
google.maps.event.addListener(that.marker, 'mouseover', mouseover);
google.maps.event.addListener(that.marker, 'mouseout', mouseout);
that.marker.addTo(that.map.map);
const element = that.marker.getElement()
if(element) {
element.addEventListener('click', click)
element.addEventListener('dblclick', dblclick)
element.addEventListener('mouseover', mouseover)
element.addEventListener('mouseout', mouseout)
}
return that;
};
@ -285,9 +307,9 @@ Ox.MapMarker = function(options) {
@*/
that.edit = function() {
setOptions();
google.maps.event.addListener(that.marker, 'dragstart', dragstart);
google.maps.event.addListener(that.marker, 'drag', drag);
google.maps.event.addListener(that.marker, 'dragend', dragend);
that.marker.on('dragstart', dragstart);
that.marker.on('drag', drag);
that.marker.on('dragend', dragend);
return that;
};
@ -296,8 +318,12 @@ Ox.MapMarker = function(options) {
() -> <f> remove marker from map, returns MapMarker
@*/
that.remove = function() {
that.marker.setMap(null);
google.maps.event.clearListeners(that.marker);
that.marker.remove();
//that.marker.off('dragstart');
//that.marker.off('drag');
//that.marker.off('dragend');
//fixme does this work to remove all events?
that.marker.off();
return that;
};
@ -306,9 +332,9 @@ Ox.MapMarker = function(options) {
() -> <f> clear edit listeners, returns MapMarker
@*/
that.submit = function() {
google.maps.event.clearListeners(that.marker, 'dragstart');
google.maps.event.clearListeners(that.marker, 'drag');
google.maps.event.clearListeners(that.marker, 'dragend');
that.marker.off('dragstart');
that.marker.off('drag');
that.marker.off('dragend');
return that;
}

View file

@ -30,7 +30,7 @@ Ox.MapMarkerImage = (function() {
themeData = Ox.Theme.getThemeData();
if (!cache[index]) {
var color = options.rectangle ? [0, 0, 0, 0]
var color = options.rectangle ? [255, 255, 255, 1]
: options.color.concat(
[options.type == 'place' ? 0.75 : 0.25]
),
@ -50,12 +50,10 @@ Ox.MapMarkerImage = (function() {
c.context.strokeStyle = 'rgba(' + border.join(', ') + ')';
c.context.arc(r, r, r - 1, 0, 360);
c.context.stroke();
cache[index] = new google.maps.MarkerImage(
c.canvas.toDataURL(),
new google.maps.Size(options.size, options.size),
new google.maps.Point(0, 0),
new google.maps.Point(r, r)
);
cache[index] = document.createElement('img')
cache[index].src = c.canvas.toDataURL()
cache[index].width = options.size
cache[index].height = options.size
}
return cache[index];

View file

@ -23,6 +23,7 @@ Ox.MapPlace = function(options) {
options = Ox.extend({
east: 0,
editable: true,
editing: false,
geoname: '',
map: null,
@ -44,21 +45,24 @@ Ox.MapPlace = function(options) {
update();
function update(updateMarker) {
if (that.west > that.east) {
that.east += 360;
}
that.points = {
ne: new google.maps.LatLng(that.north, that.east),
sw: new google.maps.LatLng(that.south, that.west)
ne: new maplibregl.LngLat(that.east, that.north),
sw: new maplibregl.LngLat(that.west, that.south)
};
that.bounds = new google.maps.LatLngBounds(that.points.sw, that.points.ne);
that.bounds = new maplibregl.LngLatBounds(that.points.sw, that.points.ne);
that.center = that.bounds.getCenter();
that.lat = that.center.lat();
that.lng = that.center.lng();
that.lat = that.center.lat;
that.lng = that.center.lng;
Ox.extend(that.points, {
e: new google.maps.LatLng(that.lat, that.east),
s: new google.maps.LatLng(that.south, that.lng),
se: new google.maps.LatLng(that.south, that.east),
n: new google.maps.LatLng(that.north, that.lng),
nw: new google.maps.LatLng(that.north, that.west),
w: new google.maps.LatLng(that.lat, that.west)
e: new maplibregl.LngLat(that.east, that.lat),
s: new maplibregl.LngLat(that.lng, that.south),
se: new maplibregl.LngLat(that.east, that.south),
n: new maplibregl.LngLat(that.lng, that.north),
nw: new maplibregl.LngLat(that.west, that.north),
w: new maplibregl.LngLat(that.west, that.lat)
});
// fixme: use bounds.toSpan()
that.sizeNorthSouth = (that.north - that.south)
@ -79,8 +83,9 @@ Ox.MapPlace = function(options) {
place: that
});
} else if (updateMarker) {
that.marker.update();
that.rectangle.update();
console.log("fixme update marker")
//that.marker.update();
//that.rectangle.update();
}
}

View file

@ -1,5 +1,178 @@
'use strict';
class MapLibreRectangle {
constructor(options = {}) {
this.id = options.id || 'rectangle-' + Ox.uid();
this.bounds = options.bounds;
this.draggable = options.draggable || false;
this.onclick = options.onclick || null
}
_createRectangle() {
const coords = this._getPolygonCoordinates();
const rectangleGeoJSON = {
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: [coords]
},
properties: {
id: this.id
}
};
var sourceId = `${this.id}-rectangles`
this.source = this.map.getSource(sourceId)
if (!this.source) {
this.map.addSource(sourceId, {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: []
}
});
this.source = this.map.getSource(sourceId)
// Add fill layer
var layerId = `${this.id}-fill`
if (!this.map.getLayer(layerId)) {
this.map.addLayer({
id: layerId,
type: 'fill',
source: sourceId,
paint: {
'fill-color': '#088',
'fill-opacity': 0.3
}
});
}
// Add outline layer
var layerId = `${this.id}-outline`
if (!this.map.getLayer(layerId)) {
this.map.addLayer({
id: layerId,
type: 'line',
source: sourceId,
paint: {
'line-color': '#000',
'line-width': 2
}
});
}
}
/*
this.source._data.features.push(rectangleGeoJSON)
this.source.setData(this.source._data)
*/
}
_getPolygonCoordinates() {
const sw = this.bounds._sw;
const ne = this.bounds._ne;
return [
[sw.lng, ne.lat], // NW
[ne.lng, ne.lat], // NE
[ne.lng, sw.lat], // SE
[sw.lng, sw.lat], // SW
[sw.lng, ne.lat] // Close polygon
];
}
setBounds(bounds) {
this.bounds = bounds;
const coords = this._getPolygonCoordinates();
const updatedData = {
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: [coords]
},
properties: {
id: this.id
}
};
var updated = false;
this.source._data.features.forEach(feature => {
if (feature.properties.id == this.id) {
feature.geometry = updatedData.geometry
updated = true
}
})
if (!updated) {
this.source._data.features.push(updatedData)
}
this.source.setData(this.source._data)
}
getBounds() {
return this.bounds;
}
_enableDragging() {
let isDragging = false;
let startPos;
this.map.on('mousedown', `${this.id}-fill`, (e) => {
e.preventDefault();
isDragging = true;
startPos = e.lngLat;
this.map.getCanvas().style.cursor = 'grabbing';
});
this.map.on('mousemove', (e) => {
if (!isDragging) return;
const dx = e.lngLat.lng - startPos.lng;
const dy = e.lngLat.lat - startPos.lat;
const sw = [this.bounds[0][0] + dx, this.bounds[0][1] + dy];
const ne = [this.bounds[1][0] + dx, this.bounds[1][1] + dy];
this.setBounds([sw, ne]);
startPos = e.lngLat;
});
this.map.on('mouseup', () => {
if (isDragging) {
isDragging = false;
this.map.getCanvas().style.cursor = '';
}
});
}
_enableClicking() {
this.map.on('click', `${this.id}-fill`, e => {
console.log('click', e)
if (this.onclick) {
e.preventDefault()
//e.stopPropagation()
this.onclick(e)
}
})
}
remove() {
this.source._data.features = this.source._data.features.filter(feature => {
return feature.properties.id != this.id
})
this.source.setData(this.source._data)
}
add() {
this.setBounds(this.bounds)
}
addTo(map) {
this.map = map;
this._createRectangle();
if (this.draggable) this._enableDragging();
this._enableClicking();
}
}
/*@
Ox.MapRectangle <f> MapRectangle Object
(options) -> <o> MapRectangle Object
@ -25,14 +198,23 @@ Ox.MapRectangle = function(options) {
/*@
rectangle <f> google.maps.Rectangle
@*/
/*
that.rectangle = new google.maps.Rectangle({
clickable: true,
bounds: that.place.bounds
});
*/
that.rectangle = new MapLibreRectangle({
bounds: that.place.bounds,
});
that.rectangle.addTo(that.map.map);
that.rectangle.onclick = click
/*@
markers <a> array of markers
markers <a> array of markers (only corners for rectangle resizing)
@*/
that.markers = Ox.map(that.place.points, function(point, position) {
var cornerPositions = ['ne', 'nw', 'se', 'sw'];
that.markers = cornerPositions.map(function(position) {
return new Ox.MapRectangleMarker({
map: that.map,
place: that.place,
@ -42,7 +224,7 @@ Ox.MapRectangle = function(options) {
setOptions();
function click() {
function click(e) {
if (
that.map.options('editable')
&& that.place.editable
@ -64,6 +246,7 @@ Ox.MapRectangle = function(options) {
? 'mapPlaceEditingBorder'
: 'mapPlaceSelectedBorder'
]);
/*
that.rectangle.setOptions({
bounds: that.place.bounds,
fillColor: color,
@ -72,14 +255,24 @@ Ox.MapRectangle = function(options) {
strokeOpacity: that.place.id[0] == '_' ? 0.5 : 1,
strokeWeight: 2
});
*/
/*
console.log("fixme", {
bounds: that.place.bounds,
fillColor: color,
fillOpacity: that.place.editing ? 0.1 : 0,
strokeColor: color,
strokeOpacity: that.place.id[0] == '_' ? 0.5 : 1,
strokeWeight: 2
});
*/
}
/*@
add <f> add
@*/
that.add = function() {
that.rectangle.setMap(that.map.map);
google.maps.event.addListener(that.rectangle, 'click', click);
that.rectangle.add()
return that;
};
@ -99,8 +292,7 @@ Ox.MapRectangle = function(options) {
remove <f> remove
@*/
that.remove = function() {
that.rectangle.setMap(null);
google.maps.event.clearListeners(that.rectangle);
that.rectangle.remove();
return that;
}
@ -121,6 +313,8 @@ Ox.MapRectangle = function(options) {
that.update = function() {
Ox.Log('Map', 'UPDATE...')
setOptions();
// Update the visual rectangle bounds
that.rectangle.setBounds(that.place.bounds);
Ox.forEach(that.markers, function(marker) {
marker.update();
});

View file

@ -23,6 +23,7 @@ Ox.MapRectangleMarker = function(options) {
that[key] = val;
});
/*
that.markerImage = new google.maps.MarkerImage
that.marker = new google.maps.Marker({
cursor: that.position + '-resize',
@ -35,20 +36,39 @@ Ox.MapRectangleMarker = function(options) {
position: that.place.points[that.position],
raiseOnDrag: false
});
*/
// Create a simple DOM element for the corner handle
var element = document.createElement('div');
element.style.width = '8px';
element.style.height = '8px';
element.style.backgroundColor = 'white';
element.style.border = '2px solid black';
element.style.borderRadius = '2px';
element.style.cursor = that.position + '-resize';
element.style.boxSizing = 'border-box';
that.marker = new maplibregl.Marker({
draggable: true,
element: element,
});
that.marker.setLngLat(that.place.points[that.position])
function dragstart(e) {
Ox.$body.addClass('OxDragging');
// In MapLibre GL, get position from marker directly
var lngLat = that.marker.getLngLat();
that.drag = {
lat: e.latLng.lat(),
lng: e.latLng.lng()
lat: lngLat.lat,
lng: lngLat.lng
};
}
function drag(e) {
// fixme: implement shift+drag (center stays the same)
Ox.Log('Map', e.pixel.x, e.pixel.y)
var lat = Ox.limit(e.latLng.lat(), Ox.MIN_LATITUDE, Ox.MAX_LATITUDE),
lng = e.latLng.lng();
// In MapLibre GL, get current position from marker directly
var lngLat = that.marker.getLngLat();
var lat = Ox.limit(lngLat.lat, Ox.MIN_LATITUDE, Ox.MAX_LATITUDE),
lng = lngLat.lng;
that.drag = {
lat: lat,
lng: lng
@ -90,32 +110,33 @@ Ox.MapRectangleMarker = function(options) {
add <f> add
@*/
that.add = function() {
that.marker.setMap(that.map.map);
google.maps.event.addListener(that.marker, 'dragstart', dragstart);
google.maps.event.addListener(that.marker, 'drag', drag);
google.maps.event.addListener(that.marker, 'dragend', dragend);
that.marker.addTo(that.map.map);
that.marker.on('dragstart', dragstart);
that.marker.on('drag', drag);
that.marker.on('dragend', dragend);
return that;
};
/*@
remove <f> remove
@*/
that.remove = function() {
that.marker.setMap(null);
google.maps.event.clearListeners(that.marker);
// Clean up MapLibre events
that.marker.off('dragstart');
that.marker.off('drag');
that.marker.off('dragend');
// Remove marker from map
that.marker.remove();
return that;
};
/*@
update <f> update
@*/
that.update = function() {
that.marker.setOptions({
icon: Ox.MapMarkerImage({
mode: 'editing',
rectangle: true,
type: that.place.id[0] == '_' ? 'result' : 'place'
}),
position: that.place.points[that.position]
});
// Just update position - visual stays the same during editing
that.marker.setLngLat(that.place.points[that.position]);
return that;
};
return that;

View file

@ -0,0 +1,284 @@
/* Basics */
.maplibregl-ctrl-geocoder,
.maplibregl-ctrl-geocoder *,
.maplibregl-ctrl-geocoder *:after,
.maplibregl-ctrl-geocoder *:before {
box-sizing: border-box;
}
.maplibregl-ctrl-geocoder {
font-size: 18px;
line-height: 24px;
font-family: "Open Sans", "Helvetica Neue", Arial, Helvetica, sans-serif;
position: relative;
background-color: #fff;
width: 100%;
min-width: 240px;
z-index: 1;
border-radius: 4px;
transition: width 0.25s, min-width 0.25s;
}
.maplibregl-ctrl-geocoder--input {
font: inherit;
width: 100%;
border: 0;
background-color: transparent;
margin: 0;
height: 50px;
color: #404040; /* fallback */
color: rgba(0, 0, 0, 0.75);
padding: 6px 45px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.maplibregl-ctrl-geocoder--input::-ms-clear {
display: none; /* hide input clear button in IE */
}
.maplibregl-ctrl-geocoder--input:focus {
color: #404040; /* fallback */
color: rgba(0, 0, 0, 0.75);
outline: 0;
box-shadow: none;
outline: thin dotted;
}
.maplibregl-ctrl-geocoder .maplibregl-ctrl-geocoder--pin-right > * {
z-index: 2;
position: absolute;
right: 8px;
top: 7px;
display: none;
}
.maplibregl-ctrl-geocoder,
.maplibregl-ctrl-geocoder .suggestions {
box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);
}
/* Collapsed */
.maplibregl-ctrl-geocoder.maplibregl-ctrl-geocoder--collapsed {
width: 50px;
min-width: 50px;
transition: width 0.25s, min-width 0.25s;
}
/* Suggestions */
.maplibregl-ctrl-geocoder .suggestions {
background-color: #fff;
border-radius: 4px;
left: 0;
list-style: none;
margin: 0;
padding: 0;
position: absolute;
width: 100%;
top: 110%; /* fallback */
top: calc(100% + 6px);
z-index: 1000;
overflow: hidden;
font-size: 15px;
}
.maplibregl-ctrl-bottom-left .suggestions,
.maplibregl-ctrl-bottom-right .suggestions {
top: auto;
bottom: 100%;
}
.maplibregl-ctrl-geocoder .suggestions > li > a {
cursor: default;
display: block;
padding: 6px 12px;
color: #404040;
}
.maplibregl-ctrl-geocoder .suggestions > .active > a,
.maplibregl-ctrl-geocoder .suggestions > li > a:hover {
color: #404040;
background-color: #f3f3f3;
text-decoration: none;
cursor: pointer;
}
.maplibregl-ctrl-geocoder--suggestion {
display: flex;
flex-direction: row;
align-items: center;
}
.maplibre-ctrl-geocoder--suggestion-icon {
min-width: 30px;
min-height: 24px;
max-width: 30px;
max-height: 24px;
padding-right: 12px;
}
.maplibregl-ctrl-geocoder--suggestion-info {
display: flex;
flex-direction: column;
}
.maplibregl-ctrl-geocoder--suggestion-match {
font-weight: bold;
}
.maplibregl-ctrl-geocoder--suggestion-title,
.maplibregl-ctrl-geocoder--suggestion-address {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.maplibregl-ctrl-geocoder--result {
display: flex;
flex-direction: row;
align-items: center;
}
.maplibre-ctrl-geocoder--result-icon {
min-width: 30px;
min-height: 24px;
max-width: 30px;
max-height: 24px;
padding-right: 12px;
}
.maplibregl-ctrl-geocoder--result-title {
font-weight: bold;
}
.maplibregl-ctrl-geocoder--result-title,
.maplibregl-ctrl-geocoder--result-address {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
/* Icons */
.maplibregl-ctrl-geocoder--icon {
display: inline-block;
vertical-align: middle;
speak: none;
fill: #757575;
top: 15px;
}
.maplibregl-ctrl-geocoder--icon-search {
position: absolute;
top: 13px;
left: 12px;
width: 23px;
height: 23px;
}
.maplibregl-ctrl-geocoder--button {
padding: 0;
margin: 0;
border: none;
cursor: pointer;
background: #fff;
line-height: 1;
}
.maplibregl-ctrl-geocoder--icon-close {
width: 20px;
height: 20px;
margin-top: 8px;
margin-right: 3px;
}
.maplibregl-ctrl-geocoder--button:hover .maplibregl-ctrl-geocoder--icon-close {
fill: #909090;
}
.maplibregl-ctrl-geocoder--icon-loading {
width: 26px;
height: 26px;
margin-top: 5px;
margin-right: 0px;
-moz-animation: rotate 0.8s infinite cubic-bezier(0.45, 0.05, 0.55, 0.95);
-webkit-animation: rotate 0.8s infinite cubic-bezier(0.45, 0.05, 0.55, 0.95);
animation: rotate 0.8s infinite cubic-bezier(0.45, 0.05, 0.55, 0.95);
}
/* Animation */
@-webkit-keyframes rotate {
from {
-webkit-transform: rotate(0);
transform: rotate(0);
}
to {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes rotate {
from {
-webkit-transform: rotate(0);
transform: rotate(0);
}
to {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
/* Media queries*/
@media screen and (min-width: 640px) {
.maplibregl-ctrl-geocoder.maplibregl-ctrl-geocoder--collapsed {
width: 36px;
min-width: 36px;
}
.maplibregl-ctrl-geocoder {
width: 33.3333%;
font-size: 15px;
line-height: 20px;
max-width: 360px;
}
.maplibregl-ctrl-geocoder .suggestions {
font-size: 13px;
}
.maplibregl-ctrl-geocoder--icon {
top: 8px;
}
.maplibregl-ctrl-geocoder--icon-close {
width: 16px;
height: 16px;
margin-top: 3px;
margin-right: 0;
}
.maplibregl-ctrl-geocoder--icon-search {
left: 7px;
width: 20px;
height: 20px;
}
.maplibregl-ctrl-geocoder--input {
height: 36px;
padding: 6px 35px;
}
.maplibregl-ctrl-geocoder--icon-loading {
width: 26px;
height: 26px;
margin-top: -2px;
margin-right: -5px;
}
.maplibre-gl-geocoder--error {
color: #909090;
padding: 6px 12px;
font-size: 16px;
text-align: center;
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -92,13 +92,15 @@ def build_oxjs(downloads=False, geo=False):
# copy & link
ui_files = {'dev': [], 'min': []}
css_files = ['maplibre-gl.css', 'maplibre-gl-geocoder.css']
for path, dirnames, filenames in os.walk(source_path):
for filename in filenames:
if '_' not in path and filename[0] not in '._' \
and not filename.endswith('~') \
and not filename.endswith('.css') \
and '/UI/svg' not in path \
and (geo or '/Geo/' not in path):
if filename.endswith('.css') and filename not in css_files:
continue
# write copies in min path
source = os.path.join(path, filename)
is_jquery = re.search(r'^jquery-[\d\.]+\.js$', filename)