code from amapei
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
package react.map;
|
||||
import react.ReactComponent;
|
||||
import react.ReactMacro.jsx;
|
||||
import Common;
|
||||
import leaflet.L;
|
||||
|
||||
using Lambda;
|
||||
|
||||
|
||||
/**
|
||||
* Externs for react-leaflet
|
||||
* @doc https://react-leaflet.js.org/docs/en/intro.html
|
||||
*/
|
||||
@:jsRequire('react-leaflet', 'Map')
|
||||
extern class LeafMap extends ReactComponent {}
|
||||
@:jsRequire('react-leaflet', 'TileLayer')
|
||||
extern class TileLayer extends ReactComponent {}
|
||||
@:jsRequire('react-leaflet', 'Marker')
|
||||
extern class Marker extends ReactComponent {}
|
||||
@:jsRequire('react-leaflet', 'CircleMarker')
|
||||
extern class CircleMarker extends ReactComponent {}
|
||||
@:jsRequire('react-leaflet', 'Popup')
|
||||
extern class Popup extends ReactComponent {}
|
||||
@:jsRequire('react-leaflet', 'FeatureGroup')
|
||||
extern class FeatureGroup extends ReactComponent {}
|
||||
@:jsRequire('react-leaflet', 'LayerGroup')
|
||||
extern class LayerGroup extends ReactComponent {}
|
||||
|
||||
|
||||
/*
|
||||
extern class L2 {
|
||||
static function icon(a:Dynamic):Dynamic;
|
||||
static function latLng(lat:Float, lng:Float):Dynamic;
|
||||
}*/
|
||||
|
||||
/**
|
||||
* GroupItem
|
||||
* @author rcrestey
|
||||
*/
|
||||
typedef GroupMapProps = {
|
||||
var addressCoord:Dynamic;
|
||||
var groups:Array<GroupOnMap>;
|
||||
var fetchGroupsInsideBox:Box->Void;
|
||||
var groupFocusedId:Int;
|
||||
};
|
||||
|
||||
typedef GroupMapState = {
|
||||
var isFitting:Bool;
|
||||
var focusedMarker:Dynamic;
|
||||
};
|
||||
|
||||
typedef Box = {
|
||||
var minLat:Float;
|
||||
var maxLat:Float;
|
||||
var minLng:Float;
|
||||
var maxLng:Float;
|
||||
};
|
||||
|
||||
class GroupMap extends ReactComponentOfPropsAndState<GroupMapProps, GroupMapState> {
|
||||
static inline var DEFAULT_LAT = 46.52863469527167; // center of France
|
||||
static inline var DEFAULT_LNG = 2.43896484375; // center of France
|
||||
static inline var INIT_ZOOM = 6;
|
||||
static inline var DEFAULT_ZOOM = 13;
|
||||
|
||||
var map:Dynamic;
|
||||
var featureGroup:Dynamic;
|
||||
var markerMap = new Map<Int,Dynamic>();
|
||||
|
||||
var groupIcon = L.icon({
|
||||
iconUrl: '/img/marker.svg',
|
||||
iconSize: [40, 40],
|
||||
iconAnchor: [20, 40],
|
||||
popupAnchor: [0, -30],
|
||||
className: 'icon'
|
||||
});
|
||||
|
||||
var homeIcon = L.icon({
|
||||
iconUrl: '/img/home.svg',
|
||||
iconSize: [40, 40],
|
||||
iconAnchor: [20, 20],
|
||||
popupAnchor: [0, -30],
|
||||
className: 'icon'
|
||||
});
|
||||
|
||||
function new() {
|
||||
super();
|
||||
state = {
|
||||
isFitting: false,
|
||||
focusedMarker: null
|
||||
};
|
||||
}
|
||||
|
||||
function getMap(element:Dynamic):Void {
|
||||
map = element.leafletElement;
|
||||
}
|
||||
|
||||
function getFeatureGroup(element:Dynamic):Void {
|
||||
featureGroup = element.leafletElement;
|
||||
setState({
|
||||
isFitting: true
|
||||
}, fitBounds);
|
||||
}
|
||||
|
||||
function getMarker(element:Dynamic, id:Int):Void {
|
||||
if (element != null)
|
||||
markerMap.set(id, element.leafletElement);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Call API to get groups in the current bounding box
|
||||
*/
|
||||
function getGroups() {
|
||||
var bounds = map.getBounds();
|
||||
var southWest = bounds.getSouthWest();
|
||||
var northEast = bounds.getNorthEast();
|
||||
|
||||
props.fetchGroupsInsideBox({
|
||||
minLat: southWest.lat,
|
||||
maxLat: northEast.lat,
|
||||
minLng: southWest.lng,
|
||||
maxLng: northEast.lng
|
||||
});
|
||||
}
|
||||
|
||||
function fitBounds() {
|
||||
map.fitBounds(featureGroup.getBounds(), {
|
||||
padding: [30, 30]
|
||||
});
|
||||
}
|
||||
|
||||
function handleMoveEnd() {
|
||||
if (
|
||||
props.addressCoord != null &&
|
||||
!Lambda.empty(props.groups) &&
|
||||
map.distance(map.getCenter(), props.addressCoord) == 0
|
||||
)
|
||||
setState({
|
||||
isFitting: true
|
||||
}, fitBounds);
|
||||
else if (state.isFitting)
|
||||
setState({
|
||||
isFitting: false
|
||||
});
|
||||
else
|
||||
getGroups();
|
||||
}
|
||||
|
||||
override public function componentDidMount() {
|
||||
if (props.addressCoord == null)
|
||||
getGroups();
|
||||
}
|
||||
|
||||
override public function shouldComponentUpdate(nextProps:GroupMapProps, nextState:GroupMapState) {
|
||||
if (nextState.focusedMarker != state.focusedMarker)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
override public function componentDidUpdate(prevProps:GroupMapProps, prevState:GroupMapState) {
|
||||
if (props.groupFocusedId != null) {
|
||||
if (
|
||||
prevProps.groupFocusedId != props.groupFocusedId
|
||||
|| state.focusedMarker == null
|
||||
) {
|
||||
if (state.focusedMarker != null)
|
||||
state.focusedMarker.closePopup();
|
||||
|
||||
var focusedMarker = markerMap.get(props.groupFocusedId);
|
||||
focusedMarker.openPopup();
|
||||
|
||||
setState({
|
||||
focusedMarker: focusedMarker
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (prevProps.groupFocusedId != null && state.focusedMarker != null) {
|
||||
state.focusedMarker.closePopup();
|
||||
|
||||
setState({
|
||||
focusedMarker: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
override public function render() {
|
||||
var center = props.addressCoord == null
|
||||
? L.latLng(DEFAULT_LAT, DEFAULT_LNG)
|
||||
: props.addressCoord;
|
||||
|
||||
var zoom = props.addressCoord == null
|
||||
? INIT_ZOOM
|
||||
: DEFAULT_ZOOM;
|
||||
|
||||
return jsx('
|
||||
<LeafMap
|
||||
center=${center}
|
||||
zoom=${zoom}
|
||||
ref=${getMap}
|
||||
onMoveEnd=${handleMoveEnd}
|
||||
>
|
||||
<TileLayer
|
||||
attribution="&copy <a href="http://osm.org/copyright">OpenStreetMap</a> contributors"
|
||||
url="https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoiYnViYXIiLCJhIjoiY2loM2lubmZpMDBwcGtxbHlwdmw0bXRkbCJ9.rfgXPakoGnXZ3wIGA3-1kQ"
|
||||
id="bubar.cih3inmqd00tjuxm7oc2532l0"
|
||||
/>
|
||||
<FeatureGroup ref=${getFeatureGroup}>
|
||||
${renderGroupMarkers()}
|
||||
${renderHomeMarker()}
|
||||
</FeatureGroup>
|
||||
</LeafMap>
|
||||
');
|
||||
}
|
||||
|
||||
function renderGroupMarkers() {
|
||||
var markers = props.groups.map(function(group) {
|
||||
var coord = [group.place.latitude, group.place.longitude];
|
||||
|
||||
function markerGetter(e:Dynamic) {
|
||||
getMarker(e, group.place.id);
|
||||
}
|
||||
|
||||
var image = group.image==null ? null : jsx('<img className="groupImage img-responsive" src=${group.image}/>');
|
||||
|
||||
return jsx('
|
||||
<Marker
|
||||
position=${coord}
|
||||
ref=${markerGetter}
|
||||
key=${group.place.id}
|
||||
icon=${groupIcon}
|
||||
>
|
||||
<Popup className="popup">
|
||||
<div>
|
||||
<a href=${"/group/"+group.id} target="_blank">
|
||||
$image
|
||||
<div className="groupName">${group.name}</div>
|
||||
</a>
|
||||
</div>
|
||||
</Popup>
|
||||
</Marker>
|
||||
');
|
||||
});
|
||||
|
||||
return jsx('<div>${markers}</div>');
|
||||
}
|
||||
|
||||
function renderHomeMarker() {
|
||||
if (props.addressCoord != null)
|
||||
return jsx('<Marker position=${props.addressCoord} icon=${homeIcon} />');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
package react.map;
|
||||
import js.Promise;
|
||||
import react.ReactComponent;
|
||||
import react.ReactMacro.jsx;
|
||||
import utils.HttpUtil;
|
||||
import leaflet.L;
|
||||
import Common;
|
||||
using Lambda;
|
||||
|
||||
@:jsRequire('react-places-autocomplete', 'default')
|
||||
extern class Autocomplete extends ReactComponent {}
|
||||
|
||||
@:jsRequire('react-places-autocomplete')
|
||||
extern class GeoUtil {
|
||||
static function geocodeByAddress(address:Dynamic):Promise<Dynamic>;
|
||||
}
|
||||
|
||||
@:jsRequire('geolib')
|
||||
extern class Geolib {
|
||||
static function getDistance(start:Dynamic, end:Dynamic):Float;
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups Map
|
||||
* @author rcrestey
|
||||
*/
|
||||
|
||||
typedef GroupMapRootState = {
|
||||
var point:Dynamic;
|
||||
var address:String;
|
||||
var groups:Array<GroupOnMap>;
|
||||
var groupFocusedId:Int;
|
||||
var isInit:Bool;
|
||||
};
|
||||
|
||||
typedef GroupMapRootProps = {
|
||||
var lat:Float;
|
||||
var lng:Float;
|
||||
var address:String;
|
||||
};
|
||||
|
||||
class GroupMapRoot extends ReactComponentOfState<GroupMapRootState>{
|
||||
|
||||
static inline var GROUP_MAP_URL = '/api/group/map';
|
||||
|
||||
var distanceMap = new Map<Int,Dynamic>();
|
||||
|
||||
public function new(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
state = {
|
||||
point: L.latLng(props.lat, props.lng),
|
||||
address: props.address,
|
||||
groups: [],
|
||||
groupFocusedId: null,
|
||||
isInit: false
|
||||
};
|
||||
}
|
||||
|
||||
function onChange(address) {
|
||||
setState({
|
||||
address: address
|
||||
});
|
||||
}
|
||||
|
||||
function openPopup(group:Dynamic) {
|
||||
setState({
|
||||
groupFocusedId: group.place.id
|
||||
});
|
||||
}
|
||||
|
||||
function closePopup() {
|
||||
setState({
|
||||
groupFocusedId: null
|
||||
});
|
||||
}
|
||||
|
||||
function geocodeByAddress(address:String):Promise<Dynamic> {
|
||||
return GeoUtil.geocodeByAddress(address)
|
||||
.then(function(results) {
|
||||
var lat = results[0].geometry.location.lat();
|
||||
var lng = results[0].geometry.location.lng();
|
||||
|
||||
return {lat: lat, lng: lng};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Call API to find groups at $lat and $lng
|
||||
* @param lat -
|
||||
* @param lng -
|
||||
*/
|
||||
function fetchGroups(lat:Float, lng:Float) {
|
||||
HttpUtil.fetch(GROUP_MAP_URL, GET, {lat: lat, lng: lng}, JSON)
|
||||
.then(function(results) {
|
||||
setState({
|
||||
point: L.latLng(lat, lng),
|
||||
groups: results.groups,
|
||||
isInit: true
|
||||
}, fillDistanceMap);
|
||||
})
|
||||
.catchError(function(error) {
|
||||
trace('Error', error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Call API to look for groups in the defined bounding box
|
||||
*/
|
||||
var wait : Bool;
|
||||
function fetchGroupsInsideBox(newBox) {
|
||||
|
||||
switch(wait){
|
||||
case null,false : wait = true;
|
||||
case true : trace("stop"); return;
|
||||
}
|
||||
|
||||
HttpUtil.fetch(GROUP_MAP_URL, GET, newBox, JSON)
|
||||
.then(function(results) {
|
||||
wait = false;
|
||||
setState({
|
||||
groups: results.groups
|
||||
}, fillDistanceMap);
|
||||
});
|
||||
/*.catchError(function(error) {
|
||||
trace('Error', error + " stack:"+haxe.CallStack.toString(haxe.CallStack.exceptionStack())) ;
|
||||
wait = false;
|
||||
});*/
|
||||
}
|
||||
|
||||
function getGroupDistance(group:GroupOnMap):Float {
|
||||
if (state.point == null)
|
||||
return null;
|
||||
|
||||
var start = {
|
||||
latitude: state.point.lat,
|
||||
longitude: state.point.lng
|
||||
};
|
||||
var end = {
|
||||
latitude: group.place.latitude,
|
||||
longitude: group.place.longitude
|
||||
};
|
||||
|
||||
return Geolib.getDistance(start, end);
|
||||
}
|
||||
|
||||
function fillDistanceMap() {
|
||||
for (group in state.groups) {
|
||||
distanceMap.set(group.place.id, getGroupDistance(group));
|
||||
}
|
||||
|
||||
orderGroupsByDistance(state.groups);
|
||||
|
||||
setState({
|
||||
groups: state.groups
|
||||
});
|
||||
}
|
||||
|
||||
function orderGroupsByDistance(groups:Array<GroupOnMap>) {
|
||||
groups.sort(function(a, b) {
|
||||
return distanceMap.get(a.place.id) - distanceMap.get(b.place.id);
|
||||
});
|
||||
}
|
||||
|
||||
function convertDistance(distance:Int):String { // to test
|
||||
if (distance > 10000)
|
||||
return Math.floor(distance / 1000) + ' km';
|
||||
if (distance > 1000)
|
||||
return Math.floor(distance / 100) / 10 + ' km';
|
||||
return distance + ' m';
|
||||
}
|
||||
|
||||
function handleSelect(address:String) {
|
||||
setState({
|
||||
address: address
|
||||
});
|
||||
|
||||
geocodeByAddress(address)
|
||||
.then(function(coord) {
|
||||
fetchGroups(coord.lat, coord.lng);
|
||||
})
|
||||
.catchError(function(error) {
|
||||
trace('Error', error);
|
||||
});
|
||||
}
|
||||
|
||||
override public function componentDidMount() {
|
||||
if (state.point != null)
|
||||
fetchGroups(state.point.lat, state.point.lng);
|
||||
else if (state.address != '')
|
||||
handleSelect(state.address);
|
||||
}
|
||||
|
||||
function renderSuggestion(obj:Dynamic) {
|
||||
return jsx('
|
||||
<div className="autocomplete-item">
|
||||
<i className="fa fa-map-marker autocomplete-icon" />
|
||||
<strong>${obj.formattedSuggestion.mainText}</strong>
|
||||
<small className="text-muted">${obj.formattedSuggestion.secondaryText}</small>
|
||||
</div>
|
||||
');
|
||||
}
|
||||
|
||||
override public function render() {
|
||||
var inputProps = {
|
||||
value: state.address,
|
||||
onChange: onChange
|
||||
};
|
||||
|
||||
var cssClasses = {
|
||||
root: 'form-group',
|
||||
input: 'autocomplete-input',
|
||||
autocompleteContainer: 'autocomplete-results',
|
||||
};
|
||||
|
||||
return jsx('
|
||||
<div className="group-map">
|
||||
<div className="row">
|
||||
<div id="logo" className="col-md-3"> </div>
|
||||
<div className="col-md-9">
|
||||
<div className="form-group-container">
|
||||
Trouvez un groupe Cagette près de chez vous
|
||||
<Autocomplete
|
||||
inputProps=${inputProps}
|
||||
onSelect=${handleSelect}
|
||||
classNames=${cssClasses}
|
||||
renderSuggestion=${renderSuggestion}
|
||||
placeHolder="Saisissez votre adresse"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-md-3" id="groupsContainer">${renderGroupList()}</div>
|
||||
<div className="col-md-9" id="mapContainer">${renderGroupMap()}</div>
|
||||
</div>
|
||||
</div>');
|
||||
}
|
||||
|
||||
function renderGroupMap() {
|
||||
if (!state.isInit)
|
||||
return null;
|
||||
|
||||
return jsx('
|
||||
<GroupMap
|
||||
addressCoord=${state.point}
|
||||
groups=${state.groups}
|
||||
fetchGroupsInsideBox=${fetchGroupsInsideBox}
|
||||
groupFocusedId=${state.groupFocusedId}
|
||||
/>
|
||||
');
|
||||
}
|
||||
|
||||
function renderGroupList() {
|
||||
var groups = state.groups.map(function(group) {
|
||||
return renderGroup(group);
|
||||
});
|
||||
|
||||
return jsx('
|
||||
<div className="groups">
|
||||
${groups}
|
||||
</div>
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a group in the left list
|
||||
*/
|
||||
function renderGroup(group:GroupOnMap) {
|
||||
var address = [
|
||||
group.place.address1,
|
||||
group.place.address2,
|
||||
[group.place.zipCode, group.place.city].join(" "),
|
||||
];
|
||||
|
||||
var addressBlock = Lambda.array(address.mapi(function(index, element) {
|
||||
if (element != null){
|
||||
return jsx('<div key=${index}>$element</div>');
|
||||
}else{
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
|
||||
var distance = null;
|
||||
if (distanceMap.get(group.place.id) != null)
|
||||
distance = jsx('<div className="distance">${convertDistance(distanceMap.get(group.place.id))}</div>');
|
||||
|
||||
var classNames = ['clickable groupBlock'];
|
||||
if (group.place.id == state.groupFocusedId)
|
||||
classNames.push('focused');
|
||||
|
||||
var img = if(group.image==null) {
|
||||
null;
|
||||
}else{
|
||||
jsx('<img src="${group.image}" className="img-responsive" />');
|
||||
}
|
||||
|
||||
return jsx('<a target="_blank"
|
||||
onMouseEnter=${function() { openPopup(group); }}
|
||||
onMouseLeave=${closePopup}
|
||||
className=${classNames.join(' ')}
|
||||
key=${group.place.id}
|
||||
href=${"/group/"+group.id}
|
||||
>
|
||||
$img
|
||||
<h4>${group.name}</h4>
|
||||
<div className="address">${addressBlock}</div>
|
||||
${distance}
|
||||
</a>');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user