homeflowjs 0.12.24 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/actions/properties.actions.js +2 -2
- package/actions/properties.types.js +1 -1
- package/images/replaceImageDimensions.js +2 -0
- package/package.json +1 -1
- package/properties/properties-map/draggable-map.js +43 -148
- package/properties/properties-map/drawable-map.js +1 -3
- package/properties/properties-map/geonames-map.js +415 -0
- package/properties/properties-map/geonames.css +40 -0
- package/properties/properties-map/map-loader.js +23 -0
- package/properties/properties-map/properties-map.component.jsx +5 -1
- package/reducers/properties.reducer.js +3 -2
- package/reducers/properties.reducer.test.js +4 -4
- package/search/property-search/property-search.js +5 -0
- package/shared/oval.svg +16 -0
@@ -243,7 +243,7 @@ export const setSelectedMarker = (payload) => ({
|
|
243
243
|
payload,
|
244
244
|
});
|
245
245
|
|
246
|
-
export const
|
247
|
-
type: PropertiesActionTypes.
|
246
|
+
export const setGeonames = (payload) => ({
|
247
|
+
type: PropertiesActionTypes.SET_GEONAMES,
|
248
248
|
payload,
|
249
249
|
});
|
@@ -8,7 +8,7 @@ const PropertiesActionTypes = {
|
|
8
8
|
SET_PAGINATION: 'SET_PAGINATION',
|
9
9
|
SET_PROPERTY_LINKS: 'SET_PROPERTY_LINKS',
|
10
10
|
SET_SELECTED_MARKER: 'SET_SELECTED_MARKER',
|
11
|
-
|
11
|
+
SET_GEONAMES: 'SET_GEONAMES',
|
12
12
|
};
|
13
13
|
|
14
14
|
export default PropertiesActionTypes;
|
package/package.json
CHANGED
@@ -7,7 +7,6 @@ import { buildQueryString } from '../../search/property-search/property-search';
|
|
7
7
|
import { setPlace, setSearchField } from '../../actions/search.actions';
|
8
8
|
import { setProperties, setSelectedMarker, setPagination } from '../../actions/properties.actions';
|
9
9
|
import { setLoading } from '../../actions/app.actions';
|
10
|
-
import { currentGeonameId } from '../../utils/index';
|
11
10
|
|
12
11
|
const element = function (X, Y) {
|
13
12
|
this.X = X;
|
@@ -24,21 +23,23 @@ export default class DraggableMap {
|
|
24
23
|
this.render = this.render.bind(this);
|
25
24
|
this.generateMap = this.generateMap.bind(this);
|
26
25
|
this.properties = this.getProperties();
|
27
|
-
this.
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
if (newSelectedPropertyID !== stateSelectedPropertyID) {
|
36
|
-
this.activateMarker(newSelectedMarker);
|
37
|
-
}
|
38
|
-
})
|
39
|
-
}
|
26
|
+
this.initOnMarkerClick();
|
27
|
+
}
|
28
|
+
|
29
|
+
initOnMarkerClick() {
|
30
|
+
if (!Homeflow.get('select_marker_on_click')) return;
|
31
|
+
|
32
|
+
this.selectedMarker = null;
|
40
33
|
|
41
|
-
|
34
|
+
store.subscribe(() => {
|
35
|
+
const newSelectedMarker = store.getState().properties.selectedMarker;
|
36
|
+
const newSelectedPropertyID = newSelectedMarker?.property?.property_id;
|
37
|
+
const stateSelectedPropertyID = this.selectedMarker?.property?.property_id;
|
38
|
+
|
39
|
+
if (newSelectedMarker && newSelectedPropertyID && newSelectedPropertyID !== stateSelectedPropertyID) {
|
40
|
+
this.activateMarker(newSelectedMarker);
|
41
|
+
}
|
42
|
+
});
|
42
43
|
}
|
43
44
|
|
44
45
|
getSearch() {
|
@@ -58,7 +59,7 @@ export default class DraggableMap {
|
|
58
59
|
this.render();
|
59
60
|
this.buildPolygon();
|
60
61
|
this.setMarkers();
|
61
|
-
if (this.noLocationfound
|
62
|
+
if (this.noLocationfound) { this.setToMarkeredBounds(); }
|
62
63
|
if (Homeflow.get('custom_map_zoom')) {
|
63
64
|
this.map.setZoom(Homeflow.get('custom_map_zoom'));
|
64
65
|
}
|
@@ -69,11 +70,6 @@ export default class DraggableMap {
|
|
69
70
|
}
|
70
71
|
|
71
72
|
render() {
|
72
|
-
// let lastSearch;
|
73
|
-
// if (lastSearch = Ctesius.getUserHistoryCollection().last()) {
|
74
|
-
// Ctesius.storedPlace = lastSearch.get('place');
|
75
|
-
// }
|
76
|
-
|
77
73
|
this.generateMap();
|
78
74
|
if (!Homeflow.get('free_text_search')) {
|
79
75
|
this.map.on('zoomend', () => (this.onMapDrag()));
|
@@ -92,17 +88,11 @@ export default class DraggableMap {
|
|
92
88
|
}
|
93
89
|
|
94
90
|
activateMarker(marker) {
|
95
|
-
if (this.selectedMarker) {
|
91
|
+
if (this.selectedMarker?.property) {
|
96
92
|
this.selectedMarker.setIcon(this.generateMarkerIcon(this.selectedMarker.property));
|
97
93
|
}
|
98
94
|
this.selectedMarker = marker;
|
99
95
|
if (this.selectedMarker) this.selectedMarker.setIcon(this.generateMarkerIcon(marker.property));
|
100
|
-
|
101
|
-
if (marker.property.geoname_id && marker.property.viewport) {
|
102
|
-
this.bounds = this.getViewportBounds(marker.property.viewport);
|
103
|
-
this.map.fitBounds(this.bounds, Homeflow.get('custom_map_bounds_padding'));
|
104
|
-
this.onMapDrag({ markerPlaceId: marker.property.geoname_id });
|
105
|
-
}
|
106
96
|
}
|
107
97
|
|
108
98
|
generateMarkerIcon(property) {
|
@@ -153,6 +143,7 @@ export default class DraggableMap {
|
|
153
143
|
}
|
154
144
|
|
155
145
|
marker.on('click', e => Homeflow.kickEvent('map_marker_clicked', e.target));
|
146
|
+
|
156
147
|
if (Homeflow.get('select_marker_on_click')) {
|
157
148
|
marker.on('click', e => store.dispatch(setSelectedMarker(e.target)));
|
158
149
|
}
|
@@ -260,44 +251,29 @@ export default class DraggableMap {
|
|
260
251
|
const radius = Homeflow.get('custom_clustering_radius') || 10;
|
261
252
|
|
262
253
|
this.clusteringMarkerLayer = new L.MarkerClusterGroup({ maxClusterRadius: radius, showCoverageOnHover: false });
|
263
|
-
this.marker_layer = new L.MarkerClusterGroup({ maxClusterRadius: radius, showCoverageOnHover: false });
|
264
254
|
this.nonClusteringMarkerLayer = L.featureGroup();
|
255
|
+
this.marker_layer = new L.MarkerClusterGroup({ maxClusterRadius: radius, showCoverageOnHover: false });
|
265
256
|
}
|
266
257
|
|
267
258
|
setMarkers() {
|
268
259
|
this.initLayers();
|
269
260
|
|
270
|
-
if (!this.properties
|
271
|
-
|
272
|
-
|
273
|
-
if (this.isDisplayProperties()) {
|
274
|
-
this.properties?.map(property => this.setPropertyMarker(property));
|
275
|
-
} else {
|
276
|
-
this.breadcrumbs?.map(breadcrumb => this.setBreadcrumbMarker(breadcrumb));
|
277
|
-
}
|
261
|
+
if (!this.properties) { return null }
|
278
262
|
|
263
|
+
this.properties.map(property => this.setPropertyMarker(property));
|
279
264
|
this.clusteringMarkerLayer.addTo(this.map);
|
280
|
-
this.nonClusteringMarkerLayer.addTo(this.map);
|
281
265
|
|
282
266
|
return this.bounds;
|
283
267
|
}
|
284
268
|
|
285
269
|
setPropertyMarker(property) {
|
286
|
-
const geonameId = property.geoname_ids?.slice(-1)[0];
|
287
270
|
const layer = Homeflow.get('pin_clustering') ? this.clusteringMarkerLayer : this.nonClusteringMarkerLayer;
|
288
271
|
|
289
272
|
if (property.property_id === null || property.lat === 0 || property.lng === 0) return;
|
290
|
-
if (geonameId && Homeflow.get('breadcrumbs_map') && geonameId !== currentGeonameId() && !this.isDisplayProperties()) return;
|
291
273
|
|
292
274
|
layer.addLayer(this.generateMarker(property));
|
293
275
|
}
|
294
276
|
|
295
|
-
setBreadcrumbMarker(breadcrumb) {
|
296
|
-
if (!breadcrumb.lat || !breadcrumb.lng) return;
|
297
|
-
|
298
|
-
this.nonClusteringMarkerLayer.addLayer(this.generateMarker(breadcrumb));
|
299
|
-
}
|
300
|
-
|
301
277
|
setToMarkeredBounds() {
|
302
278
|
const viewport = store.getState().search.currentSearch.place?.viewport;
|
303
279
|
|
@@ -323,17 +299,10 @@ export default class DraggableMap {
|
|
323
299
|
if (this.bounds && this.bounds.isValid()) {
|
324
300
|
return this.bounds;
|
325
301
|
} else {
|
326
|
-
|
327
|
-
|
328
|
-
if (viewportBounds.flat().find(coordinate => coordinate === Infinity)) return null;
|
329
|
-
|
330
|
-
return L.latLngBounds(...viewportBounds.map(bound => L.latLng(...bound)));
|
331
|
-
} else {
|
332
|
-
const bounds = [this.getTopRightMarkerCoordinates(), this.getBottomLeftMarkerCoordinates()];
|
333
|
-
if (bounds.flat().find(coordinate => coordinate === Infinity)) return null;
|
302
|
+
const bounds = [this.getTopRightMarkerCoordinates(), this.getBottomLeftMarkerCoordinates()];
|
303
|
+
if (bounds.flat().find(coordinate => coordinate === Infinity)) return null;
|
334
304
|
|
335
|
-
|
336
|
-
}
|
305
|
+
return L.latLngBounds(...bounds.map(bound => L.latLng(...bound)));
|
337
306
|
}
|
338
307
|
}
|
339
308
|
|
@@ -344,15 +313,15 @@ export default class DraggableMap {
|
|
344
313
|
|
345
314
|
getTopRightMarkerCoordinates() {
|
346
315
|
return [
|
347
|
-
Math.max(...this.parseCoordinateArray(
|
348
|
-
Math.max(...this.parseCoordinateArray(
|
316
|
+
Math.max(...this.parseCoordinateArray(this.properties, 'lat')),
|
317
|
+
Math.max(...this.parseCoordinateArray(this.properties, 'lng'))
|
349
318
|
];
|
350
319
|
}
|
351
320
|
|
352
321
|
getBottomLeftMarkerCoordinates() {
|
353
322
|
return [
|
354
|
-
Math.min(...this.parseCoordinateArray(
|
355
|
-
Math.min(...this.parseCoordinateArray(
|
323
|
+
Math.min(...this.parseCoordinateArray(this.properties, 'lat')),
|
324
|
+
Math.min(...this.parseCoordinateArray(this.properties, 'lng'))
|
356
325
|
];
|
357
326
|
}
|
358
327
|
|
@@ -362,18 +331,6 @@ export default class DraggableMap {
|
|
362
331
|
return items.filter(item => item).map(item => parseFloat(item[coordinateType])).filter(Number);
|
363
332
|
}
|
364
333
|
|
365
|
-
isDisplayProperties() {
|
366
|
-
const { properties: { pagination: { total_count: totalCount } } } = store.getState();
|
367
|
-
|
368
|
-
if (!this.breadcrumbs || this.breadcrumbs?.length === 0 || !Homeflow.get('breadcrumbs_map')) {
|
369
|
-
return true;
|
370
|
-
} else if (this.properties && totalCount <= this.properties.length) {
|
371
|
-
return true;
|
372
|
-
} else {
|
373
|
-
return false;
|
374
|
-
}
|
375
|
-
}
|
376
|
-
|
377
334
|
buildSubPolygon(polygon, extraPolygon, style = null) {
|
378
335
|
let bounds = [];
|
379
336
|
const points = [];
|
@@ -494,32 +451,25 @@ export default class DraggableMap {
|
|
494
451
|
Homeflow.kickEvent('before_draggable_map_updated');
|
495
452
|
url = url + '&zoom_level=' + this.map.getZoom();
|
496
453
|
|
497
|
-
if (markerPlaceId) {
|
498
|
-
url = url + `&marker_place_id=${markerPlaceId}`;
|
499
|
-
}
|
500
|
-
|
501
454
|
return fetch(url)
|
502
455
|
.then((response) => response.json())
|
503
456
|
.then(json => {
|
504
457
|
Homeflow.kickEvent('draggable_map_updated', json);
|
505
|
-
store.dispatch(setProperties(json.properties))
|
506
458
|
this._running_update = false;
|
507
|
-
|
508
|
-
if (json.breadcrumbs) {
|
509
|
-
this.breadcrumbs = this.concatenateDistinctObjects('geoname_id', this.breadcrumbs, json.breadcrumbs);
|
510
|
-
this.properties = [];
|
511
|
-
} else if (json.properties) {
|
512
|
-
this.properties = this.concatenateDistinctObjects('property_id', this.properties, json.properties);
|
513
|
-
this.breadcrumbs = [];
|
514
|
-
}
|
515
|
-
// TODO: figure out what 'tile view' is
|
516
|
-
// if (this.tile_view != null) {
|
517
|
-
// this.tile_view = new Ctesius.Views.Tiles({ collection: this.collection });
|
518
|
-
// }
|
519
|
-
return this.setMarkers();
|
459
|
+
this.setSearchResponse(json);
|
520
460
|
});
|
521
461
|
}
|
522
462
|
|
463
|
+
setSearchResponse(json) {
|
464
|
+
store.dispatch(setProperties(json.properties));
|
465
|
+
|
466
|
+
if (json.properties) {
|
467
|
+
this.properties = json.properties;
|
468
|
+
}
|
469
|
+
|
470
|
+
this.setMarkers();
|
471
|
+
}
|
472
|
+
|
523
473
|
// this method was copied directly from transpiled ES5 from the browser as it's the only
|
524
474
|
// way I could get it to work.
|
525
475
|
buildDrawnPolygon(layer) {
|
@@ -654,49 +604,20 @@ export default class DraggableMap {
|
|
654
604
|
|
655
605
|
Homeflow.kickEvent('before_draggable_map_updated'); //use to show some loading?
|
656
606
|
|
657
|
-
// if (Homeflow.get('get_geo_features')) {
|
658
|
-
// $.get(geo_url, (res, status, xhr) => {
|
659
|
-
// this._running_update = false;
|
660
|
-
// if (this.geo_marker_layer != null) { this.map.removeLayer(this.geo_marker_layer); }
|
661
|
-
// return this.addGeoMarkers(res);
|
662
|
-
// });
|
663
|
-
// }
|
664
|
-
|
665
|
-
// TODO: Rewrite this to use fetch() and update redux store
|
666
607
|
fetch(url)
|
667
608
|
.then((response) => response.json())
|
668
609
|
.then((json) => {
|
669
|
-
store.dispatch(setProperties(json.properties));
|
670
610
|
store.dispatch(setPagination(json.pagination))
|
671
611
|
// TODO: implement user history
|
672
612
|
// Ctesius.getUserHistoryCollection().addSearch(s);
|
673
613
|
Homeflow.kickEvent('clear_search_box');
|
674
614
|
Homeflow.kickEvent('draggable_map_updated', json);
|
675
615
|
this._running_update = false;
|
676
|
-
this.properties = json.properties;
|
677
616
|
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
return this.setMarkers();
|
617
|
+
if (json.properties) {
|
618
|
+
setSearchResponse(json);
|
619
|
+
}
|
683
620
|
})
|
684
|
-
// return $.get(url, (res, status, xhr) => {
|
685
|
-
// s.set('performed_data', res);
|
686
|
-
// Ctesius.getUserHistoryCollection().addSearch(s);
|
687
|
-
// Homeflow.kickEvent('clear_search_box');
|
688
|
-
// Homeflow.kickEvent('draggable_map_updated', res);
|
689
|
-
// this._running_update = false;
|
690
|
-
// if (this.marker_layer != null) { this.map.removeLayer(this.marker_layer); }
|
691
|
-
// this.collection = new Ctesius.Collections.Properties(res.properties);
|
692
|
-
// Ctesius.getPropertiesCollection().add(res.properties);
|
693
|
-
// if (this.tile_view != null) {
|
694
|
-
// this.tile_view = new Ctesius.Views.Tiles({ collection: this.collection });
|
695
|
-
// }
|
696
|
-
// else { }
|
697
|
-
// //@tile_view.collection(@collection)
|
698
|
-
// return this.setMarkers();
|
699
|
-
// });
|
700
621
|
}
|
701
622
|
|
702
623
|
|
@@ -729,30 +650,4 @@ export default class DraggableMap {
|
|
729
650
|
|
730
651
|
return new L.LatLng(y, x);
|
731
652
|
}
|
732
|
-
|
733
|
-
handleBreadcrumbsInit() {
|
734
|
-
if (this.breadcrumbsInitialized) return;
|
735
|
-
|
736
|
-
const { properties: { breadcrumbs } } = store.getState();
|
737
|
-
|
738
|
-
if (breadcrumbs && this.breadcrumbs !== breadcrumbs) {
|
739
|
-
this.breadcrumbs = breadcrumbs;
|
740
|
-
this.setMarkers();
|
741
|
-
this.setToMarkeredBounds();
|
742
|
-
}
|
743
|
-
|
744
|
-
this.breadcrumbsInitialized = true;
|
745
|
-
}
|
746
|
-
|
747
|
-
concatenateDistinctObjects(key, array, otherArray) {
|
748
|
-
return [...new Map((array || []).concat(otherArray || []).map((object) => [object[key], object])).values()];
|
749
|
-
}
|
750
|
-
|
751
|
-
markerPropertyIdOrGeonameId(marker) {
|
752
|
-
if (!marker) return null;
|
753
|
-
if (marker?.property?.property_id) return marker.property.property_id;
|
754
|
-
if (marker?.property?.geoname_id) return marker.property.geoname_id;
|
755
|
-
|
756
|
-
return null;
|
757
|
-
}
|
758
653
|
}
|
@@ -22,9 +22,7 @@ export default class DrawableMap extends DraggableMap {
|
|
22
22
|
this.repositionDrawControls();
|
23
23
|
// note: I'm not 100% sure this is will ever be required if super.init() is called
|
24
24
|
// something to review in homeflow_next
|
25
|
-
|
26
|
-
return this.onMapDrag();
|
27
|
-
}
|
25
|
+
this.onMapDrag();
|
28
26
|
}
|
29
27
|
|
30
28
|
onMapDrag({ markerPlaceId } = { markerPlaceId: null }) {
|
@@ -0,0 +1,415 @@
|
|
1
|
+
/* eslint-disable */
|
2
|
+
import DraggableMap from './draggable-map';
|
3
|
+
import store from '../../store';
|
4
|
+
import { currentGeonameId } from '../../utils/index';
|
5
|
+
import { setProperties, setGeonames, setSelectedMarker } from '../../actions/properties.actions';
|
6
|
+
import { setInitialSearch, setPlace, setSearchField } from '../../actions/search.actions';
|
7
|
+
import { buildQueryString } from '../../search/property-search/property-search';
|
8
|
+
import mapLoader from './map-loader';
|
9
|
+
import './geonames.css';
|
10
|
+
|
11
|
+
export default class GeonamesMap extends DraggableMap {
|
12
|
+
constructor() {
|
13
|
+
super();
|
14
|
+
|
15
|
+
if (!Homeflow.get('custom_map_zoom_location')) Homeflow.set('custom_map_zoom_location', 'topright');
|
16
|
+
|
17
|
+
this.geonameMarkerLayers = [];
|
18
|
+
this.geonameMarkerHoverLayers = [];
|
19
|
+
}
|
20
|
+
|
21
|
+
initOnMarkerClick() {
|
22
|
+
super.initOnMarkerClick();
|
23
|
+
|
24
|
+
store.subscribe(() => {
|
25
|
+
const newSelectedMarker = store.getState().properties.selectedMarker;
|
26
|
+
const newSelectedGeonameID = newSelectedMarker?.geoname?.geoname_id;
|
27
|
+
const stateSelectedGeonameID = this.selectedMarker?.geoname?.geoname_id;
|
28
|
+
|
29
|
+
if (newSelectedGeonameID && newSelectedGeonameID !== stateSelectedGeonameID) {
|
30
|
+
this.activateGeonameMarker(newSelectedMarker);
|
31
|
+
}
|
32
|
+
});
|
33
|
+
}
|
34
|
+
|
35
|
+
init() {
|
36
|
+
this.element = Homeflow.get('draggable_map_view');
|
37
|
+
this.renderLoader();
|
38
|
+
this.showLoader();
|
39
|
+
this.render();
|
40
|
+
this.geonamesInit();
|
41
|
+
|
42
|
+
/**
|
43
|
+
* The map tries to render once before the geonames exist in Redux
|
44
|
+
* We must wait for these to be populated before initializing the
|
45
|
+
* geonames again, then unsubscribe.
|
46
|
+
*/
|
47
|
+
const prevGeonames = store.getState().properties.geonames;
|
48
|
+
const prevProperties = store.getState().properties.properties;
|
49
|
+
|
50
|
+
this.unsubscribeFromGeonamesChanges = store.subscribe(() => {
|
51
|
+
const { geonames, properties } = store.getState().properties
|
52
|
+
|
53
|
+
if (properties?.length) this.geonamesInit();
|
54
|
+
|
55
|
+
if (prevGeonames === undefined && geonames !== undefined) {
|
56
|
+
this.geonamesInit();
|
57
|
+
this.unsubscribeFromGeonamesChanges();
|
58
|
+
}
|
59
|
+
});
|
60
|
+
|
61
|
+
this.map.on('zoomend', () => {
|
62
|
+
this.setMarkerTypeforZoomLevel()
|
63
|
+
|
64
|
+
this.setMarkers();
|
65
|
+
});
|
66
|
+
|
67
|
+
/**
|
68
|
+
* when the user drags the map, we need to clear the place ID so subsequent
|
69
|
+
* searches are not restricted to that place ID. We also clear the query so
|
70
|
+
* it's clear to the user this is no longer a search for that location.
|
71
|
+
*/
|
72
|
+
this.map.on('dragend', () => {
|
73
|
+
store.dispatch(setPlace(null))
|
74
|
+
store.dispatch(setSearchField({ q: '' }));
|
75
|
+
});
|
76
|
+
|
77
|
+
/**
|
78
|
+
* If for some reason the markers have not rendered after a few seconds
|
79
|
+
* render them so we don't have an empty map
|
80
|
+
*/
|
81
|
+
setTimeout(() => {
|
82
|
+
if (!this.markersInitialized) {
|
83
|
+
this.setMarkerTypeforZoomLevel();
|
84
|
+
this.setMarkers();
|
85
|
+
this.setToMarkeredBounds();
|
86
|
+
}
|
87
|
+
}, 5000);
|
88
|
+
}
|
89
|
+
|
90
|
+
setMarkerTypeforZoomLevel() {
|
91
|
+
let zoomLevel = this.map.getZoom();
|
92
|
+
|
93
|
+
if (!zoomLevel) zoomLevel = window.lastZoomLevel;
|
94
|
+
|
95
|
+
if (zoomLevel > 9) {
|
96
|
+
window.markerType = 'properties';
|
97
|
+
} else {
|
98
|
+
window.markerType = 'geonames';
|
99
|
+
}
|
100
|
+
|
101
|
+
window.lastZoomLevel = zoomLevel
|
102
|
+
}
|
103
|
+
|
104
|
+
renderLoader() {
|
105
|
+
const loader = document.getElementById('geonames-map-loader');
|
106
|
+
if (loader) return loader.remove();
|
107
|
+
|
108
|
+
document.querySelector('.js-geo-map-wrapper')?.insertAdjacentHTML('beforeend', mapLoader);
|
109
|
+
}
|
110
|
+
|
111
|
+
showLoader() {
|
112
|
+
const loader = document.getElementById('geonames-map-loader');
|
113
|
+
loader?.classList.remove('hidden');
|
114
|
+
}
|
115
|
+
|
116
|
+
hideLoader() {
|
117
|
+
const loader = document.getElementById('geonames-map-loader');
|
118
|
+
loader?.classList.add('hidden');
|
119
|
+
}
|
120
|
+
|
121
|
+
setToMarkeredBounds() {
|
122
|
+
return super.setToMarkeredBounds();
|
123
|
+
}
|
124
|
+
|
125
|
+
getViewportBounds(viewport) {
|
126
|
+
return super.getViewportBounds(viewport);
|
127
|
+
}
|
128
|
+
|
129
|
+
generateMarkerIcon(propertyOrGeoname) {
|
130
|
+
return super.generateMarkerIcon(propertyOrGeoname);
|
131
|
+
}
|
132
|
+
|
133
|
+
initLayers() {
|
134
|
+
const maxClusterRadius = Homeflow.get('custom_clustering_radius') || 10;
|
135
|
+
|
136
|
+
if (this.propertiesLayer) this.map.removeLayer(this.propertiesLayer);
|
137
|
+
if (this.geonamesLayer) this.map.removeLayer(this.geonamesLayer);
|
138
|
+
|
139
|
+
this.propertiesLayer = new L.MarkerClusterGroup({ maxClusterRadius, showCoverageOnHover: false });
|
140
|
+
this.geonamesLayer = new L.FeatureGroup();
|
141
|
+
}
|
142
|
+
|
143
|
+
hideSuggestedDestinations() {
|
144
|
+
const el = document.querySelector('.suggested-destinations');
|
145
|
+
|
146
|
+
if (el) el.style.display = 'none';
|
147
|
+
}
|
148
|
+
|
149
|
+
showSuggestedDestinations() {
|
150
|
+
const el = document.querySelector('.suggested-destinations');
|
151
|
+
|
152
|
+
if (el) el.style.display = 'block';
|
153
|
+
}
|
154
|
+
|
155
|
+
geonamesInit() {
|
156
|
+
if (!this.map || this.geonamesInitialized) return;
|
157
|
+
|
158
|
+
const { geonames } = store.getState().properties;
|
159
|
+
|
160
|
+
if (geonames && !this.geonames) {
|
161
|
+
this.geonamesInitialized = true;
|
162
|
+
this.geonames = geonames;
|
163
|
+
}
|
164
|
+
|
165
|
+
this.setMarkers();
|
166
|
+
this.setToMarkeredBounds();
|
167
|
+
|
168
|
+
const zoomLevel = this.map.getZoom();
|
169
|
+
|
170
|
+
// If we don't have any geonames we need to show property markers
|
171
|
+
if (!geonames && zoomLevel > 9) window.markerType = 'properties';
|
172
|
+
|
173
|
+
/**
|
174
|
+
* If we don't have any geonames we need to run onMapDrag
|
175
|
+
* as it doesn't run for open searches otherwise and we need
|
176
|
+
* it to load geonames
|
177
|
+
*/
|
178
|
+
if (!geonames && zoomLevel < 10) {
|
179
|
+
this.onMapDrag();
|
180
|
+
window.markerType = 'geonames';
|
181
|
+
}
|
182
|
+
}
|
183
|
+
|
184
|
+
getMarkerBounds() {
|
185
|
+
if (this.viewport) {
|
186
|
+
const viewportBounds = this.getViewportBounds();
|
187
|
+
if (viewportBounds.flat().find(coordinate => coordinate === Infinity)) return null;
|
188
|
+
|
189
|
+
return L.latLngBounds(...viewportBounds.map(bound => L.latLng(...bound)));
|
190
|
+
} else {
|
191
|
+
return super.getMarkerBounds();
|
192
|
+
}
|
193
|
+
}
|
194
|
+
|
195
|
+
getTopRightMarkerCoordinates() {
|
196
|
+
return [
|
197
|
+
Math.max(...this.parseCoordinateArray([this.properties, this.geonames].flat(), 'lat')),
|
198
|
+
Math.max(...this.parseCoordinateArray([this.properties, this.geonames].flat(), 'lng'))
|
199
|
+
];
|
200
|
+
}
|
201
|
+
|
202
|
+
getBottomLeftMarkerCoordinates() {
|
203
|
+
return [
|
204
|
+
Math.min(...this.parseCoordinateArray([this.properties, this.geonames].flat(), 'lat')),
|
205
|
+
Math.min(...this.parseCoordinateArray([this.properties, this.geonames].flat(), 'lng'))
|
206
|
+
];
|
207
|
+
}
|
208
|
+
|
209
|
+
setSearchResponse(json) {
|
210
|
+
if (!json) return;
|
211
|
+
|
212
|
+
if (json.geonames) {
|
213
|
+
this.geonames = this.concatenateDistinctObjects('geoname_id', this.geonames, json.geonames);
|
214
|
+
window.markerType = 'geonames';
|
215
|
+
store.dispatch(setGeonames(json.geonames));
|
216
|
+
} else if (json.properties) {
|
217
|
+
this.properties = this.concatenateDistinctObjects('property_id', this.properties, json.properties);
|
218
|
+
window.markerType = 'properties';
|
219
|
+
store.dispatch(setProperties(json.properties));
|
220
|
+
}
|
221
|
+
|
222
|
+
if (json.location) {
|
223
|
+
store.dispatch(setPlace(json.location));
|
224
|
+
store.dispatch(setInitialSearch({ q: json.location.name }));
|
225
|
+
}
|
226
|
+
|
227
|
+
this.setMarkers();
|
228
|
+
}
|
229
|
+
|
230
|
+
setMarkers() {
|
231
|
+
this.initLayers();
|
232
|
+
|
233
|
+
if (window.markerType === 'properties' && this.properties?.length) {
|
234
|
+
this.properties.forEach(property => this.setPropertyMarker(property));
|
235
|
+
this.hideSuggestedDestinations();
|
236
|
+
this.markersInitialized = true;
|
237
|
+
} else if (window.markerType === 'geonames' && this.geonames?.length) {
|
238
|
+
this.geonames.forEach(geoname => this.setGeonameMarker(geoname));
|
239
|
+
this.showSuggestedDestinations();
|
240
|
+
this.markersInitialized = true;
|
241
|
+
}
|
242
|
+
|
243
|
+
this.geonamesLayer.addTo(this.map);
|
244
|
+
this.propertiesLayer.addTo(this.map);
|
245
|
+
}
|
246
|
+
|
247
|
+
generateMarker(propertyOrGeoname) {
|
248
|
+
const marker = L.marker(
|
249
|
+
[propertyOrGeoname.lat, propertyOrGeoname.lng],
|
250
|
+
{ title: propertyOrGeoname.name, icon: this.generateMarkerIcon(propertyOrGeoname) }
|
251
|
+
);
|
252
|
+
|
253
|
+
marker.on('click', () => {
|
254
|
+
if (marker.geoname) {
|
255
|
+
this.geonameMarkerClick(marker);
|
256
|
+
store.dispatch(setSelectedMarker(marker));
|
257
|
+
} else if (marker.property) {
|
258
|
+
this.propertyMarkerClick(marker);
|
259
|
+
store.dispatch(setSelectedMarker(marker));
|
260
|
+
}
|
261
|
+
});
|
262
|
+
|
263
|
+
if (Homeflow.get('show_geoname_polygon_on_mouseover') && propertyOrGeoname.polygon?.length) {
|
264
|
+
marker.on('mouseover', () => this.geonameMarkerMouseOver(marker));
|
265
|
+
marker.on('mouseout', () => this.geonameMarkerMouseOut(marker));
|
266
|
+
}
|
267
|
+
|
268
|
+
if (propertyOrGeoname.geoname_id) {
|
269
|
+
marker.geoname = propertyOrGeoname;
|
270
|
+
} else {
|
271
|
+
marker.property = propertyOrGeoname;
|
272
|
+
}
|
273
|
+
|
274
|
+
return marker;
|
275
|
+
}
|
276
|
+
|
277
|
+
geonameMarkerMouseOver(marker) {
|
278
|
+
const selectedGeonameId = this.selectedMarker?.geoname?.geoname_id;
|
279
|
+
|
280
|
+
if (selectedGeonameId && selectedGeonameId === marker.geoname.geoname_id) return;
|
281
|
+
|
282
|
+
this.showGeonameMarkerPolygon(
|
283
|
+
{ polygon: marker.geoname.polygon, layers: this.geonameMarkerHoverLayers }
|
284
|
+
);
|
285
|
+
};
|
286
|
+
|
287
|
+
geonameMarkerMouseOut(marker) {
|
288
|
+
this.hideGeonameMarkerPolygon({ layers: this.geonameMarkerHoverLayers });
|
289
|
+
}
|
290
|
+
|
291
|
+
setPropertyMarker(property) {
|
292
|
+
this.propertiesLayer.addLayer(this.generateMarker(property));
|
293
|
+
}
|
294
|
+
|
295
|
+
setGeonameMarker(geoname) {
|
296
|
+
if (!geoname.lat || !geoname.lng) return;
|
297
|
+
|
298
|
+
this.geonamesLayer.addLayer(this.generateMarker(geoname));
|
299
|
+
}
|
300
|
+
|
301
|
+
activateGeonameMarker(marker) {
|
302
|
+
this.selectedMarker = marker;
|
303
|
+
this.bounds = this.getViewportBounds(marker.geoname.viewport);
|
304
|
+
this.map.fitBounds(this.bounds, Homeflow.get('custom_map_bounds_padding'));
|
305
|
+
|
306
|
+
this.onMapDrag({ markerPlaceId: marker.geoname.geoname_id });
|
307
|
+
}
|
308
|
+
|
309
|
+
propertyMarkerClick(marker) {
|
310
|
+
const { properties } = store.getState().properties;
|
311
|
+
|
312
|
+
// Ensure active property is included in list view before trying to select it.
|
313
|
+
store.dispatch(
|
314
|
+
setProperties(this.concatenateDistinctObjects('property_id', properties, [marker.property]))
|
315
|
+
);
|
316
|
+
}
|
317
|
+
|
318
|
+
geonameMarkerClick(marker) {
|
319
|
+
this.showLoader();
|
320
|
+
if (!marker.geoname) return;
|
321
|
+
|
322
|
+
if (marker.geoname.polygon?.length) {
|
323
|
+
this.hideGeonameMarkerPolygon({ layers: this.geonameMarkerHoverLayers });
|
324
|
+
this.hideGeonameMarkerPolygon({ layers: this.geonameMarkerLayers });
|
325
|
+
this.showGeonameMarkerPolygon({ polygon: marker.geoname.polygon, layers: this.geonameMarkerLayers });
|
326
|
+
}
|
327
|
+
|
328
|
+
// If a location marker is clicked, allow updating search results count.
|
329
|
+
Homeflow.set('location_property_count', marker.geoname.properties_count);
|
330
|
+
|
331
|
+
// If user clicks a marker and no more markers are visible, they may expect something to
|
332
|
+
// hapen if they click the marker again. Zoom in one step to give a sense of feedback.
|
333
|
+
if (this.lastClickedGeonameId === marker.geoname.geoname_id) {
|
334
|
+
const center = new L.LatLng(marker.geoname.lat, marker.geoname.lng);
|
335
|
+
this.map.setView(center, this.map.getZoom() + 1);
|
336
|
+
} else {
|
337
|
+
this.lastClickedGeonameId = marker.geoname.geoname_id;
|
338
|
+
}
|
339
|
+
}
|
340
|
+
|
341
|
+
onMapDrag({ markerPlaceId } = { markerPlaceId: null }) {
|
342
|
+
this.showLoader();
|
343
|
+
|
344
|
+
this.mostRecentRequestId = crypto.randomUUID();
|
345
|
+
|
346
|
+
const bounds = this.getSearchableBounds();
|
347
|
+
const zoomLevel = this.map.getZoom();
|
348
|
+
const params = [
|
349
|
+
`${buildQueryString(this.getSearch())}/view-${bounds.toBBoxString()}`,
|
350
|
+
'&count=50',
|
351
|
+
`&zoom_level=${zoomLevel}`,
|
352
|
+
`&request_id=${this.mostRecentRequestId}`,
|
353
|
+
];
|
354
|
+
|
355
|
+
if (markerPlaceId) {
|
356
|
+
params.push(`&marker_place_id=${markerPlaceId}`);
|
357
|
+
}
|
358
|
+
|
359
|
+
const url = `/search.ljson?${params.join('')}`;
|
360
|
+
|
361
|
+
/**
|
362
|
+
* If the map has already loaded with a location we don't want it to
|
363
|
+
* make another request for the viewport as it will load properties
|
364
|
+
* outside the polygon so just remove the loader and return
|
365
|
+
*/
|
366
|
+
if (!this.mapRenderedOnce) {
|
367
|
+
this.hideLoader();
|
368
|
+
return this.mapRenderedOnce = true;
|
369
|
+
}
|
370
|
+
|
371
|
+
fetch(url)
|
372
|
+
.then((response) => this.processResponse(response))
|
373
|
+
.then((json) => this.setSearchResponse(json))
|
374
|
+
.finally(() => this.hideLoader())
|
375
|
+
.catch((err) => console.error(err));
|
376
|
+
}
|
377
|
+
|
378
|
+
processResponse(response) {
|
379
|
+
if (response.headers.get('X-Homeflow-Request-Id') === this.mostRecentRequestId) {
|
380
|
+
return response.json();
|
381
|
+
} else {
|
382
|
+
return Promise.resolve(null);
|
383
|
+
}
|
384
|
+
}
|
385
|
+
|
386
|
+
hideGeonameMarkerPolygon({ layers }) {
|
387
|
+
if (!layers?.length) return;
|
388
|
+
|
389
|
+
layers.forEach((layer) => this.map.removeLayer(layer));
|
390
|
+
layers.length = 0;
|
391
|
+
}
|
392
|
+
|
393
|
+
showGeonameMarkerPolygon({ polygon, layers }) {
|
394
|
+
const shapeStyle = {
|
395
|
+
clickable: false,
|
396
|
+
fill: true,
|
397
|
+
color: Homeflow.get('polygon_color', '#3388ff'),
|
398
|
+
fillColor: Homeflow.get('polygon_fill_color', null),
|
399
|
+
fillOpacity: .2,
|
400
|
+
opacity: .5,
|
401
|
+
stroke: true,
|
402
|
+
weight: 4,
|
403
|
+
};
|
404
|
+
|
405
|
+
polygon.forEach((coordinates) => {
|
406
|
+
layers.push(new L.Polygon(coordinates.map(longLat => longLat.toReversed()), shapeStyle));
|
407
|
+
});
|
408
|
+
|
409
|
+
layers.forEach((layer) => this.map.addLayer(layer));
|
410
|
+
}
|
411
|
+
|
412
|
+
concatenateDistinctObjects (key, array, otherArray) {
|
413
|
+
return [...new Map((array || []).concat(otherArray || []).map((object) => [object[key], object])).values()];
|
414
|
+
}
|
415
|
+
}
|
@@ -0,0 +1,40 @@
|
|
1
|
+
.geonames-map__loader-background {
|
2
|
+
position: absolute;
|
3
|
+
top: 0;
|
4
|
+
left: 0;
|
5
|
+
width: 100%;
|
6
|
+
bottom: 51px;
|
7
|
+
z-index: 200;
|
8
|
+
pointer-events: none;
|
9
|
+
}
|
10
|
+
|
11
|
+
.geonames-map__loader-inner {
|
12
|
+
position: relative;
|
13
|
+
top: 50%;
|
14
|
+
transform: translateY(-50%);
|
15
|
+
text-align: center;
|
16
|
+
font-weight: bold;
|
17
|
+
font-size: 2rem;
|
18
|
+
}
|
19
|
+
|
20
|
+
.geonames-map__loader.hidden {
|
21
|
+
display: none;
|
22
|
+
}
|
23
|
+
|
24
|
+
@media (min-width: 1024px) {
|
25
|
+
.geonames-map__loader-background {
|
26
|
+
top: 240px;
|
27
|
+
left: 43px;
|
28
|
+
width: 59%;
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
@media (min-width: 1400px) {
|
33
|
+
.geonames-map__loader-background {
|
34
|
+
left: 10%;
|
35
|
+
}
|
36
|
+
}
|
37
|
+
|
38
|
+
.map-cluster-marker, .map-cluster-marker svg, .map-cluster-marker path, .map-cluster-marker__count {
|
39
|
+
pointer-events: none;
|
40
|
+
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
export default `
|
2
|
+
<div id="geonames-map-loader" class="js-geonames-map-loader geonames-map__loader-background">
|
3
|
+
<div class="geonames-map__loader-inner">
|
4
|
+
<svg width="38" height="38" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg" stroke="#fff">
|
5
|
+
<g fill="none" fill-rule="evenodd">
|
6
|
+
<g transform="translate(1 1)" stroke-width="2">
|
7
|
+
<circle stroke-opacity=".5" cx="18" cy="18" r="18"/>
|
8
|
+
<path d="M36 18c0-9.94-8.06-18-18-18">
|
9
|
+
<animateTransform
|
10
|
+
attributeName="transform"
|
11
|
+
type="rotate"
|
12
|
+
from="0 18 18"
|
13
|
+
to="360 18 18"
|
14
|
+
dur="1s"
|
15
|
+
repeatCount="indefinite"
|
16
|
+
/>
|
17
|
+
</path>
|
18
|
+
</g>
|
19
|
+
</g>
|
20
|
+
</svg>
|
21
|
+
</div>
|
22
|
+
</div>
|
23
|
+
`;
|
@@ -8,6 +8,7 @@ import PropTypes from 'prop-types';
|
|
8
8
|
// TODO: Possibly lazy-load these
|
9
9
|
import DraggableMap from './draggable-map';
|
10
10
|
import DrawableMap from './drawable-map';
|
11
|
+
import GeonamesMap from './geonames-map';
|
11
12
|
|
12
13
|
import './leaflet.css';
|
13
14
|
import './leaflet.draw.css';
|
@@ -16,7 +17,10 @@ import './marker_cluster.default.css';
|
|
16
17
|
|
17
18
|
// global callback to run when maps scripts are loaded
|
18
19
|
window.initLegacyMap = () => {
|
19
|
-
if (Homeflow.get('
|
20
|
+
if (Homeflow.get('geonames_map')) {
|
21
|
+
const map = new GeonamesMap();
|
22
|
+
map.init();
|
23
|
+
} else if (Homeflow.get('enable_draw_a_map') && !!window.CanvasRenderingContext2D && !!history.pushState) {
|
20
24
|
const map = new DrawableMap();
|
21
25
|
map.init();
|
22
26
|
} else {
|
@@ -2,6 +2,7 @@ import PropertiesActionTypes from '../actions/properties.types';
|
|
2
2
|
|
3
3
|
const INITIAL_STATE = {
|
4
4
|
properties: [],
|
5
|
+
geonames: undefined,
|
5
6
|
savedProperties: [],
|
6
7
|
pagination: {},
|
7
8
|
selectedMarker: null,
|
@@ -83,10 +84,10 @@ const propertiesReducer = (state = INITIAL_STATE, action) => {
|
|
83
84
|
...state,
|
84
85
|
selectedMarker: action.payload,
|
85
86
|
};
|
86
|
-
case '
|
87
|
+
case 'SET_GEONAMES': {
|
87
88
|
return {
|
88
89
|
...state,
|
89
|
-
|
90
|
+
geonames: action.payload,
|
90
91
|
}
|
91
92
|
};
|
92
93
|
default:
|
@@ -60,11 +60,11 @@ describe('propertiesReducer', () => {
|
|
60
60
|
expect(reducedState).toMatchObject(state);
|
61
61
|
});
|
62
62
|
|
63
|
-
it('Sets
|
64
|
-
const
|
63
|
+
it('Sets geonames when it receives the SET_GEONAMES action', () => {
|
64
|
+
const geonames = [{ geoname_id: '12345' }, { geoname_id: '45678' }];
|
65
65
|
const state = { properties: [] };
|
66
|
-
const reducedState = propertiesReducer(state, { type: '
|
66
|
+
const reducedState = propertiesReducer(state, { type: 'SET_GEONAMES', payload: geonames });
|
67
67
|
|
68
|
-
expect(reducedState.
|
68
|
+
expect(reducedState.geonames).toEqual([{ geoname_id: '12345' }, { geoname_id: '45678' }]);
|
69
69
|
});
|
70
70
|
});
|
@@ -50,6 +50,11 @@ export const buildQueryString = (search) => {
|
|
50
50
|
}
|
51
51
|
});
|
52
52
|
|
53
|
+
// If we're on a map, redirect back to it
|
54
|
+
if (window.location.hash === '#/map') {
|
55
|
+
queryParams.push('hashbang=map');
|
56
|
+
}
|
57
|
+
|
53
58
|
let queryString = queryParams.join('&');
|
54
59
|
queryString += `&fragment=${fragmentParams.join('/')}`;
|
55
60
|
return queryString;
|
package/shared/oval.svg
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
<svg width="38" height="38" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg" stroke="#fff">
|
2
|
+
<g fill="none" fill-rule="evenodd">
|
3
|
+
<g transform="translate(1 1)" stroke-width="2">
|
4
|
+
<circle stroke-opacity=".5" cx="18" cy="18" r="18"/>
|
5
|
+
<path d="M36 18c0-9.94-8.06-18-18-18">
|
6
|
+
<animateTransform
|
7
|
+
attributeName="transform"
|
8
|
+
type="rotate"
|
9
|
+
from="0 18 18"
|
10
|
+
to="360 18 18"
|
11
|
+
dur="1s"
|
12
|
+
repeatCount="indefinite"/>
|
13
|
+
</path>
|
14
|
+
</g>
|
15
|
+
</g>
|
16
|
+
</svg>
|