[Asterisk-code-review] Geolocation: Core Capability Preview (asterisk[certified/18.9])

George Joseph asteriskteam at digium.com
Wed Jul 13 10:57:05 CDT 2022


George Joseph has submitted this change. ( https://gerrit.asterisk.org/c/asterisk/+/18797 )

Change subject: Geolocation:  Core Capability Preview
......................................................................

Geolocation:  Core Capability Preview

This commit adds res_geolocation which creates the core capabilities
to manipulate Geolocation information on SIP INVITEs.

An upcoming commit will add res_pjsip_geolocation which will
allow the capabilities to be used with the pjsip channel driver.

This commit message is intentionally short because this isn't
a simple capability.  See the documentation at
https://wiki.asterisk.org/wiki/display/AST/Geolocation
for more information.

THE CAPABILITIES IMPLEMENTED HERE MAY CHANGE BASED ON
USER FEEDBACK!

ASTERISK-30127

Change-Id: Ibfde963121b1ecf57fd98ee7060c4f0808416303
---
M Makefile.rules
A configs/samples/geolocation.conf.sample
A doc/CHANGES-staging/res_geolocation.txt
A include/asterisk/res_geolocation.h
M res/Makefile
A res/res_geolocation.c
A res/res_geolocation.exports.in
A res/res_geolocation/eprofile_to_pidf.xslt
A res/res_geolocation/geoloc_civicaddr.c
A res/res_geolocation/geoloc_common.c
A res/res_geolocation/geoloc_config.c
A res/res_geolocation/geoloc_datastore.c
A res/res_geolocation/geoloc_dialplan.c
A res/res_geolocation/geoloc_doc.xml
A res/res_geolocation/geoloc_eprofile.c
A res/res_geolocation/geoloc_gml.c
A res/res_geolocation/geoloc_private.h
A res/res_geolocation/pidf_lo_test.xml
A res/res_geolocation/pidf_to_eprofile.xslt
19 files changed, 4,910 insertions(+), 0 deletions(-)

Approvals:
  Kevin Harwell: Looks good to me, but someone else must approve
  George Joseph: Looks good to me, approved; Approved for Submit



diff --git a/Makefile.rules b/Makefile.rules
index 934e44a..e6b6589 100644
--- a/Makefile.rules
+++ b/Makefile.rules
@@ -204,4 +204,19 @@
 	$(ECHO_PREFIX) echo "   [LD] $^ -> $@"
 	$(CMD_PREFIX) $(CXX) -o $@ $(PTHREAD_CFLAGS) $(_ASTLDFLAGS) $^ $(CXX_LIBS) $(ASTLDFLAGS)
 
+# These CC commands just create an object file with the input file embedded in it.
+# It can be access from code as follows:
+# If your input file is named abc_def.xml...
+#
+# extern const uint8_t _binary_abc_def_xml_start[];
+# extern const uint8_t _binary_abc_def_xml_end[];
+# extern const size_t _binary_abc_def_xml_size;
+%.o: %.xml
+	$(ECHO_PREFIX) echo "   [LD] $^ -> $@"
+	$(CMD_PREFIX) $(CC) -g -nostartfiles  -nodefaultlibs -nostdlib -r -Wl,-b,binary -o $@ $^
+
+%.o: %.xslt
+	$(ECHO_PREFIX) echo "   [LD] $^ -> $@"
+	$(CMD_PREFIX) $(CC) -g -nostartfiles  -nodefaultlibs -nostdlib -r -Wl,-b,binary -o $@ $^
+
 dist-clean:: clean
diff --git a/configs/samples/geolocation.conf.sample b/configs/samples/geolocation.conf.sample
new file mode 100644
index 0000000..0566cbd
--- /dev/null
+++ b/configs/samples/geolocation.conf.sample
@@ -0,0 +1,264 @@
+;--
+  Geolocation Profile Sample Configuration
+
+--;
+
+;--
+=======================================================================
+  Overview
+=======================================================================
+
+Geolocation information is actually comprised of two objects, a
+Location object, and a Profile object.
+
+Location objects must contain one of the following:
+
+  - Location information specified in Geographic Markup Language
+    (GML) or civicAddress formats.
+
+  - A URI that points to externally hosted location information.
+
+Profile objects contain instructions for the disposition of location
+information, an optional reference to a Location object, and updates or
+overrides to that Location object if specified.
+
+Channel drivers and the dialplan functions are responsible for
+associating Profiles to endpoints/devices and calls.  Normally, two
+profiles would be assigned to an endpoint to control behavior in each
+direction and to optionally specify location information.  One for
+incoming calls (Asterisk is the UAS) and and one for outgoing calls
+(Asterisk is the UAC).
+
+NOTE:
+
+See https://wiki.asterisk.org/wiki/display/AST/Geolocation for the most
+complete and up-to-date information on valid values for the object
+parameters and a full list of references.
+
+GENERAL CAUTION:  You must coordinate with your partners with regards
+to what location information is expected by each party and how it should
+be formatted.  An outgoing configuration mismatch for instance, could
+result in misinformation or no information being sent to an emergency
+response center or even call failure for which you are solely responsible.
+--;
+
+
+;--
+=======================================================================
+  Location Object Description
+=======================================================================
+[<location_id>]
+
+-- type (required) ----------------------------------------------------
+Defines the object type.
+type = location
+
+Must be "location" to identify this configuration section as a
+Geolocation Location object.
+
+-- format (required) --------------------------------------------------
+Sets the format used to express the location.
+format = < civicAddress | GML | URI >
+
+Values:
+civicAddress: [RFC4119] [RFC5139] [RFC5491]
+              The location information will be placed in an XML document
+              conforming to the PIDF-LO standard.
+              For chan_pjsip, this will be placed in the body of
+              outgoing INVITE messages in addition to any SDP.
+
+GML:          [RFC4119] [RFC5491] [GeoShape]
+              The location information will be placed in an XML document
+              conforming to the PIDF-LO standard.
+              For chan_pjsip, this will be placed in the body of
+              outgoing INVITE messages in addition to any SDP.
+
+URI:          [RFC6442]
+              The external URI at which the the location information
+              can be found.  For chan_pjsip, this URI will be placed
+              in a "Geolocation" header in outgoing INVITE messages.
+
+There is no default.
+
+Example:
+format = civicAddress
+
+-- location_info (required) -------------------------------------------
+The location-format-specific information describing the location.
+location_info = <location_format_specific_description>
+
+For readability, multiple "location" parameters can be specified and
+they will be concatenated into one specification.  The description may
+contain replacement variables which may be the names of common channel
+variables like ${EXTEN}, channel variables you may have added in the
+dialplan, or variables you may have specified in the profile that
+references this location object.
+
+NOTE: See https://wiki.asterisk.org/wiki/display/AST/Geolocation for the
+most complete and up-to-date information on valid values for the object
+parameters and a full list of references.
+
+WARNING: Asterisk can only validate that a particular sub-parameter
+name is valid for a particular format. It can't validate the actual
+value of the sub-parameter.
+
+Example for civicAddress:
+
+location_info = country=US
+location_info = A1="New York", A3="New York", A4=Manhattan,  
+location_info = HNO=1633, PRD=W, RD=46th, STS=Street  
+location_info = PC=10222
+
+Example for GML with replacement variables:
+
+location_info = type=Point, crs=2d, pos="${mylat} ${mylon}"
+
+Example for URI with replacement variables:
+location_info = URI=https://some.company.com?number=${phone_number}
+
+-- method (optional) --------------------------------------------------
+The method used to determine the location_info
+method = <"GPS" | "A-GPS" | "Manual" | "DHCP"
+              | "Triangulation" | "Cell" | "802.11">
+
+Example:
+method = Manual
+
+-- location_source (optional) -----------------------------------------
+Original source of the location-info.
+location_source = < FQDN >
+
+The value MUST be a FQDN.  IP addresses are specifically not
+allowed.  See RFC8787.
+
+Example:
+location_source = sip1.myserver.net
+
+-- Location Example ---------------------------------------------------
+
+[mylocation]
+type = location
+format = civicAddress
+location_info = country=US
+location_info = A1="New York", A3="New York", A4=Manhattan  
+location_info = HNO=1633, PRD=W, RD=46th, STS=Street  
+location_info = PC=10222
+method = Manual
+location_source = sip1.myserver.net
+
+=======================================================================
+--;
+
+
+;--
+=======================================================================
+  Profile Object Descriptions
+=======================================================================
+[<profile_id>]
+
+-- type (required) ----------------------------------------------------
+Defines the object type.
+type = profile
+
+-- profile_action (optional) ------------------------------------------
+Sets how to reconcile incoming and configured profiles.
+profile_action = < prefer_incoming | prefer_config | discard_incoming
+    | discard_config >
+
+On an incoming call leg, "incoming" is the location description
+received in the SIP INVITE (if any) and "config" is this profile.
+
+On an outgoing call leg, "incoming" is the location description
+passed through the dialplan to this channel (if any) and "config"
+is this profile.
+
+Values:
+
+prefer_incoming:  If there's an incoming location description, use it
+                  even if there's also a configured one.
+prefer_config:    If there's a configured location description, use it
+                  even if there's also an incoming one.
+discard_incoming: Discard any incoming location description. If there's
+                  a configured one, use it.  If not, no location
+                  information is propagated.
+discard_config:   Discard any configured location description. If
+                  there's an incoming one, use it.  If not, no location
+                  information is propagated.
+
+discard_incoming is the default.
+
+Example:
+profile_action = prefer_config
+
+-- pidf_element (optional) --------------------------------------------
+PIDF-LO element in which to place the location description.
+pidf_element = < tuple | device | person >
+
+If the format is civicAddress or GML, this sets the PIDF element into
+which the location information will be placed.
+
+Values:
+tuple:  Places the information in a "tuple" element.
+device: Places the information in a "device" element.
+person: Places the information in a "person" element.
+
+Per [RFC5491], "device" is preferred and therefore the default.
+
+Example:
+pidf_element = tuple
+
+-- geolocation_routing (optional) -------------------------------------
+Sets whether the "Geolocation-Routing" header is added to outgoing
+requests.
+geolocation_routing = < yes | no >
+
+Set to "yes" to indicate that servers later in the path
+can use the location information for routing purposes.  Set to "no"
+if they should not.  If this value isn't specified, no
+"Geolocation-Routing" header will be added.
+
+Example:
+geolocation_routing = yes
+
+-- location_reference (optional) --------------------------------------
+The name of an existing Location object.
+location_reference = <location_id>
+
+The location_info_refinement and location_variables parameters below can
+be used to refine the location object for this specific profile.
+
+Example:
+location_reference = "my_building"
+
+-- location_info_refinement (optional) --------------------------------
+Location info to add to that already retrieved from the location object.
+
+location_info_refinement = <location_format_specific_description>
+
+The information in the referenced Location object can be refined on a
+per-profile basis.  For example, if the referenced Location object has a
+civicAddress for a building, you could set location_refinement to add a
+floor and room just for this profile
+
+Example:
+location_info_refinement = floor=20, room=20a2
+
+-- location_variables -------------------------------------------------
+
+If the referenced Location object uses any replacement variables, they
+can be assigned here.  There is no need to define variables that come
+from the channel using this profile.  They get assigned automatically.
+
+location_variables = myfloor=20, myroom=222
+
+-- Profile Example ----------------------------------------------------
+
+[myprofile]
+type = profile
+location_reference = mylocation
+location_info_refinement = floor=20, room=20a2
+pidf_element = tuple
+profile_action = discard_incoming
+
+=======================================================================
+--;
diff --git a/doc/CHANGES-staging/res_geolocation.txt b/doc/CHANGES-staging/res_geolocation.txt
new file mode 100644
index 0000000..5fe7316
--- /dev/null
+++ b/doc/CHANGES-staging/res_geolocation.txt
@@ -0,0 +1,4 @@
+Subject: res_geolocation
+
+Added res_geolocation which creates the core capabilities
+to manipulate Geolocation information on SIP INVITEs.
diff --git a/include/asterisk/res_geolocation.h b/include/asterisk/res_geolocation.h
new file mode 100644
index 0000000..403e6c8
--- /dev/null
+++ b/include/asterisk/res_geolocation.h
@@ -0,0 +1,353 @@
+ /*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2022, Sangoma Technologies Corporation
+ *
+ * George Joseph <gjoseph at sangoma.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+#ifndef INCLUDE_ASTERISK_RES_GEOLOCATION_H_
+#define INCLUDE_ASTERISK_RES_GEOLOCATION_H_
+
+#include "asterisk/channel.h"
+#include "asterisk/config.h"
+#include "asterisk/sorcery.h"
+#include "asterisk/xml.h"
+#include "asterisk/optional_api.h"
+
+#define AST_GEOLOC_INVALID_VALUE -1
+
+enum ast_geoloc_pidf_element {
+	AST_PIDF_ELEMENT_NONE = 0,
+	AST_PIDF_ELEMENT_TUPLE,
+	AST_PIDF_ELEMENT_DEVICE,
+	AST_PIDF_ELEMENT_PERSON,
+	AST_PIDF_ELEMENT_LAST,
+};
+
+enum ast_geoloc_format {
+	AST_GEOLOC_FORMAT_NONE = 0,
+	AST_GEOLOC_FORMAT_CIVIC_ADDRESS,
+	AST_GEOLOC_FORMAT_GML,
+	AST_GEOLOC_FORMAT_URI,
+	AST_GEOLOC_FORMAT_LAST,
+};
+
+enum ast_geoloc_action {
+	AST_GEOLOC_ACT_PREFER_INCOMING = 0,
+	AST_GEOLOC_ACT_PREFER_CONFIG,
+	AST_GEOLOC_ACT_DISCARD_INCOMING,
+	AST_GEOLOC_ACT_DISCARD_CONFIG,
+};
+
+struct ast_geoloc_location {
+	SORCERY_OBJECT(details);
+	AST_DECLARE_STRING_FIELDS(
+		AST_STRING_FIELD(method);
+		AST_STRING_FIELD(location_source);
+	);
+	enum ast_geoloc_format format;
+	struct ast_variable *location_info;
+};
+
+struct ast_geoloc_profile {
+	SORCERY_OBJECT(details);
+	AST_DECLARE_STRING_FIELDS(
+		AST_STRING_FIELD(location_reference);
+		AST_STRING_FIELD(notes);
+	);
+	enum ast_geoloc_pidf_element pidf_element;
+	enum ast_geoloc_action action;
+	int geolocation_routing;
+	struct ast_variable *location_refinement;
+	struct ast_variable *location_variables;
+	struct ast_variable *usage_rules;
+};
+
+struct ast_geoloc_eprofile {
+	AST_DECLARE_STRING_FIELDS(
+		AST_STRING_FIELD(id);
+		AST_STRING_FIELD(location_reference);
+		AST_STRING_FIELD(location_source);
+		AST_STRING_FIELD(method);
+		AST_STRING_FIELD(notes);
+	);
+	enum ast_geoloc_pidf_element pidf_element;
+	enum ast_geoloc_action action;
+	int geolocation_routing;
+	enum ast_geoloc_format format;
+	struct ast_variable *location_info;
+	struct ast_variable *location_refinement;
+	struct ast_variable *location_variables;
+	struct ast_variable *effective_location;
+	struct ast_variable *usage_rules;
+};
+
+/*!
+ * \brief Check if res_geolocation is available
+ *
+ * \return 1 if available, 0 otherwise.
+ */
+AST_OPTIONAL_API(int, ast_geoloc_is_loaded,	(void), { return 0; });
+
+/*!
+ * \brief Retrieve a geolocation location object by id.
+ *
+ * \param id Location object id.
+ *
+ * \return Location object or NULL if not found.
+ */
+AST_OPTIONAL_API(struct ast_geoloc_location *, ast_geoloc_get_location,
+		 (const char *id),
+		 { return NULL; });
+
+/*!
+ * \brief Retrieve a geolocation profile by id.
+ *
+ * \param id profile id.
+ *
+ * \return Profile or NULL if not found.
+ */
+AST_OPTIONAL_API(struct ast_geoloc_profile *, ast_geoloc_get_profile,
+		 (const char *id),
+		 { return NULL; });
+
+/*!
+ * \brief Given a civicAddress code, check whether it's valid.
+ *
+ * \param code Pointer to the code to check
+ *
+ * \return 1 if valid, 0 otherwise.
+ */
+int ast_geoloc_civicaddr_is_code_valid(const char *code);
+
+enum ast_geoloc_validate_result {
+	AST_GEOLOC_VALIDATE_INVALID_VALUE = -1,
+	AST_GEOLOC_VALIDATE_SUCCESS = 0,
+	AST_GEOLOC_VALIDATE_MISSING_SHAPE,
+	AST_GEOLOC_VALIDATE_INVALID_SHAPE,
+	AST_GEOLOC_VALIDATE_INVALID_VARNAME,
+	AST_GEOLOC_VALIDATE_NOT_ENOUGH_VARNAMES,
+	AST_GEOLOC_VALIDATE_TOO_MANY_VARNAMES,
+};
+
+const char *ast_geoloc_validate_result_to_str(enum ast_geoloc_validate_result result);
+
+/*!
+ * \brief Validate that the names of the variables in the list are valid codes or synonyms
+ *
+ * \param varlist Variable list to check.
+ * \param result[OUT] Pointer to char * to receive failing item.
+ *
+ * \return result code.
+ */
+enum ast_geoloc_validate_result ast_geoloc_civicaddr_validate_varlist(
+	const struct ast_variable *varlist, const char **result);
+
+/*!
+ * \brief Validate that the variables in the list represent a valid GML shape
+ *
+ * \param varlist Variable list to check.
+ * \param result[OUT] Pointer to char * to receive failing item.
+ *
+ * \return result code.
+ */
+enum ast_geoloc_validate_result ast_geoloc_gml_validate_varlist(const struct ast_variable *varlist,
+	const char **result);
+
+
+/*!
+ * \brief Geolocation datastore Functions
+ * @{
+ */
+
+/*!
+ * \brief Create a geoloc datastore from a profile name
+ *
+ * \param profile_name The name of the profile to use.
+ *
+ * \return The datastore.
+ */
+struct ast_datastore *ast_geoloc_datastore_create_from_profile_name(const char *profile_name);
+
+/*!
+ * \brief Create a geoloc datastore from an effective profile.
+ *
+ * \param eprofile The effective profile to use.
+ *
+ * \return The datastore.
+ */
+struct ast_datastore *ast_geoloc_datastore_create_from_eprofile(
+	struct ast_geoloc_eprofile *eprofile);
+
+/*!
+ * \brief Create an empty geoloc datastore.
+ *
+ * \param id  An id to use for the datastore.
+ *
+ * \return The datastore.
+ */
+struct ast_datastore *ast_geoloc_datastore_create(const char *id);
+
+/*!
+ * \brief Retrieve a geoloc datastore's id.
+ *
+ * \param ds The datastore
+ *
+ * \return The datastore's id.
+ */
+const char *ast_geoloc_datastore_get_id(struct ast_datastore *ds);
+
+/*!
+ * \brief Add an eprofile to a datastore
+ *
+ * \param ds       The datastore
+ * \param eprofile The eprofile to add.
+ *
+ * \return The new number of eprofiles or -1 to indicate a failure.
+ */
+int ast_geoloc_datastore_add_eprofile(struct ast_datastore *ds,
+	struct ast_geoloc_eprofile *eprofile);
+
+/*!
+ * \brief Insert an eprofile to a datastore at the specified position
+ *
+ * \param ds       The datastore
+ * \param eprofile The eprofile to add.
+ * \param index    The position to insert at.  Existing eprofiles will
+ *                 be moved up to make room.
+ *
+ * \return The new number of eprofiles or -1 to indicate a failure.
+ */
+int ast_geoloc_datastore_insert_eprofile(struct ast_datastore *ds,
+	struct ast_geoloc_eprofile *eprofile, int index);
+
+/*!
+ * \brief Retrieves the number of eprofiles in the datastore
+ *
+ * \param ds The datastore
+ *
+ * \return The number of eprofiles.
+ */
+int ast_geoloc_datastore_size(struct ast_datastore *ds);
+
+/*!
+ * \brief Sets the inheritance flag on the datastore
+ *
+ * \param ds      The datastore
+ * \param inherit 1 to allow the datastore to be inherited by other channels
+ *                0 to prevent the datastore to be inherited by other channels
+ *
+ * \return 0 if successful, -1 otherwise.
+ */
+int ast_geoloc_datastore_set_inheritance(struct ast_datastore *ds, int inherit);
+
+/*!
+ * \brief Retrieve a specific eprofile from a datastore by index
+ *
+ * \param ds The datastore
+ * \param ix The index
+ *
+ * \return The effective profile ao2 object with its reference count bumped.
+ */
+struct ast_geoloc_eprofile *ast_geoloc_datastore_get_eprofile(struct ast_datastore *ds, int ix);
+
+/*!
+ * \brief Delete a specific eprofile from a datastore by index
+ *
+ * \param ds The datastore
+ * \param ix The index
+ *
+ * \return 0 if succesful, -1 otherwise.
+ */
+int ast_geoloc_datastore_delete_eprofile(struct ast_datastore *ds, int ix);
+
+/*!
+ * \brief Retrieves the geoloc datastore from a channel, if any
+ *
+ * \param chan Channel
+ *
+ * \return datastore if found, NULL otherwise.
+ */
+struct ast_datastore *ast_geoloc_datastore_find(struct ast_channel *chan);
+
+/*!
+ *  @}
+ */
+
+/*!
+ * \brief Geolocation Effective Profile Functions
+ * @{
+ */
+
+/*!
+ * \brief Allocate a new, empty effective profile.
+ *
+ * \param name The profile's name
+ *
+ * \return The effective profile ao2 object.
+ */
+struct ast_geoloc_eprofile *ast_geoloc_eprofile_alloc(const char *name);
+
+/*!
+ * \brief Allocate a new effective profile from an existing profile.
+ *
+ * \param profile The profile to use.
+ *
+ * \return The effective profile ao2 object.
+ */
+struct ast_geoloc_eprofile *ast_geoloc_eprofile_create_from_profile(struct ast_geoloc_profile *profile);
+
+/*!
+ * \brief Allocate a new effective profile from an XML PIDF-LO document
+ *
+ * \param pidf_xmldoc       The ast_xml_doc to use.
+ * \param geoloc_uri        The URI that referenced this document.
+ * \param reference_string  An identifying string to use in error messages.
+ *
+ * \return The effective profile ao2 object.
+ */
+struct ast_geoloc_eprofile *ast_geoloc_eprofile_create_from_pidf(
+	struct ast_xml_doc *pidf_xmldoc, const char *geoloc_uri, const char *reference_string);
+
+/*!
+ * \brief Allocate a new effective profile from a URI.
+ *
+ * \param uri               The URI to use.
+ * \param reference_string  An identifying string to use in error messages.
+ *
+ * \return The effective profile ao2 object.
+ */
+struct ast_geoloc_eprofile *ast_geoloc_eprofile_create_from_uri(const char *uri,
+	const char *reference_string);
+
+const char *ast_geoloc_eprofile_to_uri(struct ast_geoloc_eprofile *eprofile,
+	struct ast_channel *chan, struct ast_str **buf, const char *ref_string);
+
+const char *ast_geoloc_eprofiles_to_pidf(struct ast_datastore *ds,
+	struct ast_channel *chan, struct ast_str **buf, const char * ref_string);
+
+/*!
+ * \brief Refresh the effective profile with any changed info.
+ *
+ * \param eprofile The eprofile to refresh.
+ *
+ * \return 0 on success, any other value on error.
+ */
+int ast_geoloc_eprofile_refresh_location(struct ast_geoloc_eprofile *eprofile);
+
+/*!
+ *  @}
+ */
+
+#endif /* INCLUDE_ASTERISK_RES_GEOLOCATION_H_ */
diff --git a/res/Makefile b/res/Makefile
index 45ef749..58797b8 100644
--- a/res/Makefile
+++ b/res/Makefile
@@ -69,6 +69,10 @@
 $(call MOD_ADD_C,res_ari_model,ari/ari_model_validators.c)
 $(call MOD_ADD_C,res_stasis_recording,stasis_recording/stored.c)
 $(call MOD_ADD_C,res_stir_shaken,$(wildcard res_stir_shaken/*.c))
+$(call MOD_ADD_C,res_geolocation,$(wildcard res_geolocation/*.c))
+
+# These are the xml and xslt files to be embedded
+res_geolocation.so: res_geolocation/pidf_lo_test.o res_geolocation/pidf_to_eprofile.o res_geolocation/eprofile_to_pidf.o
 
 res_parking.o: _ASTCFLAGS+=$(AST_NO_FORMAT_TRUNCATION)
 snmp/agent.o: _ASTCFLAGS+=-fPIC
@@ -76,3 +80,4 @@
 
 # Dependencies for res_ari_*.so are generated, so they're in this file
 include ari.make
+
diff --git a/res/res_geolocation.c b/res/res_geolocation.c
new file mode 100644
index 0000000..19dd84b
--- /dev/null
+++ b/res/res_geolocation.c
@@ -0,0 +1,125 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2022, Sangoma Technologies Corporation
+ *
+ * George Joseph <gjoseph at sangoma.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+/*** MODULEINFO
+	<depend>libxml2</depend>
+	<depend>libxslt</depend>
+	<support_level>core</support_level>
+ ***/
+
+
+#include "asterisk.h"
+#define AST_API_MODULE
+#include "asterisk/res_geolocation.h"
+#include "res_geolocation/geoloc_private.h"
+
+static int reload_module(void)
+{
+	int res = 0;
+
+	res = geoloc_civicaddr_reload();
+	if (res) {
+		return AST_MODULE_LOAD_DECLINE;
+	}
+	res = geoloc_gml_reload();
+	if (res) {
+		return AST_MODULE_LOAD_DECLINE;
+	}
+	res = geoloc_config_reload();
+	if (res) {
+		return AST_MODULE_LOAD_DECLINE;
+	}
+	res = geoloc_eprofile_reload();
+	if (res) {
+		return AST_MODULE_LOAD_DECLINE;
+	}
+	res = geoloc_dialplan_reload();
+	if (res) {
+		return AST_MODULE_LOAD_DECLINE;
+	}
+	res = geoloc_channel_reload();
+	if (res) {
+		return AST_MODULE_LOAD_DECLINE;
+	}
+
+	return AST_MODULE_LOAD_SUCCESS;
+}
+
+static int unload_module(void)
+{
+	int res = 0;
+
+	res += geoloc_channel_unload();
+	res += geoloc_dialplan_unload();
+	res += geoloc_eprofile_unload();
+	res += geoloc_config_unload();
+	res += geoloc_gml_unload();
+	res += geoloc_civicaddr_unload();
+
+	return (res != 0);
+}
+
+static int load_module(void)
+{
+	int res = 0;
+
+	res = geoloc_civicaddr_load();
+	if (res) {
+		unload_module();
+		return AST_MODULE_LOAD_DECLINE;
+	}
+
+	res = geoloc_gml_load();
+	if (res) {
+		return AST_MODULE_LOAD_DECLINE;
+	}
+
+	res = geoloc_config_load();
+	if (res) {
+		unload_module();
+		return AST_MODULE_LOAD_DECLINE;
+	}
+
+	res = geoloc_eprofile_load();
+	if (res) {
+		unload_module();
+		return AST_MODULE_LOAD_DECLINE;
+	}
+
+	res = geoloc_dialplan_load();
+	if (res) {
+		unload_module();
+		return AST_MODULE_LOAD_DECLINE;
+	}
+
+	res = geoloc_channel_load();
+	if (res) {
+		unload_module();
+		return AST_MODULE_LOAD_DECLINE;
+	}
+
+	return AST_MODULE_LOAD_SUCCESS;
+}
+
+AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS | AST_MODFLAG_LOAD_ORDER, "res_geolocation Module for Asterisk",
+	.support_level = AST_MODULE_SUPPORT_CORE,
+	.load = load_module,
+	.unload = unload_module,
+	.reload = reload_module,
+	.load_pri = AST_MODPRI_CHANNEL_DEPEND - 10,
+);
diff --git a/res/res_geolocation.exports.in b/res/res_geolocation.exports.in
new file mode 100644
index 0000000..da0a981
--- /dev/null
+++ b/res/res_geolocation.exports.in
@@ -0,0 +1,6 @@
+{
+	global:
+		LINKER_SYMBOL_PREFIXast_geo*;
+	local:
+		*;
+};
diff --git a/res/res_geolocation/eprofile_to_pidf.xslt b/res/res_geolocation/eprofile_to_pidf.xslt
new file mode 100644
index 0000000..dbfe17b
--- /dev/null
+++ b/res/res_geolocation/eprofile_to_pidf.xslt
@@ -0,0 +1,237 @@
+<?xml version="1.0"?>
+<xsl:stylesheet version="1.1"
+	xmlns:ca="urn:ietf:params:xml:ns:pidf:geopriv10:civicAddr"
+	xmlns:dm="urn:ietf:params:xml:ns:pidf:data-model"
+	xmlns:fn="http://www.w3.org/2005/xpath-functions"
+	xmlns:gbp="urn:ietf:params:xml:ns:pidf:geopriv10:basicPolicy"
+	xmlns:gml="http://www.opengis.net/gml"
+	xmlns:gp="urn:ietf:params:xml:ns:pidf:geopriv10"
+	xmlns:gs="http://www.opengis.net/pidflo/1.0"
+	xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+	xmlns:date="http://exslt.org/dates-and-times">
+
+	<xsl:output method="xml" indent="yes"/>
+	<xsl:strip-space elements="*"/>
+
+	<!-- REMINDER:  The "match" and "select" xpaths refer to the input document,
+		not the output document -->
+
+	<xsl:template match="presence">
+		<!-- xslt will take care of adding all of the namespace declarations
+			from the list above -->
+		<presence xmlns="urn:ietf:params:xml:ns:pidf" entity="{@entity}">
+			<xsl:apply-templates select="./device|tuple|person"/>
+		</presence>
+	</xsl:template>
+
+	<xsl:template match="device">
+		<dm:device>
+			<gp:geopriv>
+				<xsl:apply-templates select="./location-info"/>
+				<xsl:apply-templates select="./usage-rules"/>
+				<xsl:apply-templates select="./method"/>
+				<xsl:apply-templates select="./note-well"/>
+			</gp:geopriv>
+			<xsl:if test="./timestamp">
+				<dm:timestamp>
+					<xsl:value-of select="./timestamp"/>
+				</dm:timestamp>
+			</xsl:if>
+			<xsl:if test="./deviceID">
+				<dm:deviceID>
+					<xsl:value-of select="./deviceID"/>
+				</dm:deviceID>
+			</xsl:if>
+		</dm:device>
+	</xsl:template>
+
+	<xsl:template match="tuple">
+		<xsl:element name="tuple" namespace="urn:ietf:params:xml:ns:pidf">
+			<xsl:element name="status" namespace="urn:ietf:params:xml:ns:pidf">
+				<gp:geopriv>
+					<xsl:apply-templates select="./location-info"/>
+					<xsl:apply-templates select="./usage-rules"/>
+					<xsl:apply-templates select="./method"/>
+					<xsl:apply-templates select="./note-well"/>
+				</gp:geopriv>
+			</xsl:element>
+			<xsl:if test="./timestamp">
+				<xsl:element name="timestamp" namespace="urn:ietf:params:xml:ns:pidf">
+					<xsl:value-of select="./timestamp"/>
+				</xsl:element>
+			</xsl:if>
+		</xsl:element>
+	</xsl:template>
+
+	<xsl:template match="person">
+		<dm:person>
+			<gp:geopriv>
+				<xsl:apply-templates select="./location-info"/>
+				<xsl:apply-templates select="./usage-rules"/>
+				<xsl:apply-templates select="./method"/>
+				<xsl:apply-templates select="./note-well"/>
+			</gp:geopriv>
+			<xsl:if test="./timestamp">
+				<dm:timestamp>
+					<xsl:value-of select="./timestamp"/>
+				</dm:timestamp>
+			</xsl:if>
+		</dm:person>
+	</xsl:template>
+
+	<xsl:template match="location-info">
+		<gp:location-info>
+			<xsl:apply-templates/>
+		</gp:location-info>
+	</xsl:template>
+
+	<!-- When we're using the civicAddress format, the translation is simple.
+		We add gp:location-info and ca:civicAddress, then we just copy in
+		each element, adding the "ca" namespace -->
+
+	<xsl:template match="civicAddress/*">
+		<xsl:element name="ca:{name()}">
+			<xsl:value-of select="."/>
+		</xsl:element>
+	</xsl:template>
+
+	<xsl:template match="location-info/civicAddress">
+		<ca:civicAddress xml:lang="{@lang}">
+			<xsl:apply-templates/>
+		</ca:civicAddress>
+	</xsl:template>
+
+	<!-- All GML shapes share common processing for the "srsName" attribute -->
+	<xsl:template name="shape">
+		<xsl:choose>
+			<xsl:when test="@crs = '3d'">
+				<xsl:attribute name="srsName">urn:ogc:def:crs:EPSG::4979</xsl:attribute>
+			</xsl:when>
+			<xsl:otherwise>
+				<xsl:attribute name="srsName">urn:ogc:def:crs:EPSG::4326</xsl:attribute>
+			</xsl:otherwise>
+		</xsl:choose>
+	</xsl:template>
+
+	<!-- The GML shapes themselves.  They don't all have the same namespace unfortunately... -->
+
+	<xsl:template match="Point|Circle|Ellipse|ArcBand|Sphere|Ellipsoid">
+		<xsl:variable name="namespace">
+			<xsl:choose>
+				<xsl:when test="name() = 'Point'">
+					<xsl:value-of select="'gml'"/>
+				</xsl:when>
+				<xsl:otherwise>
+					<xsl:value-of select="'gs'"/>
+				</xsl:otherwise>
+			</xsl:choose>
+		</xsl:variable>
+
+		<xsl:element name="{$namespace}:{name()}">
+			<xsl:call-template name="shape"/>
+			<xsl:apply-templates select="./*"/>
+		</xsl:element>
+	</xsl:template>
+
+	<!-- ... and some are more complex than others. -->
+
+	<xsl:template match="Polygon">
+		<gml:Polygon>
+			<xsl:call-template name="shape"/>
+			<gml:exterior>
+				<gml:LinearRing>
+					<xsl:apply-templates select="./pos|posList"/>
+				</gml:LinearRing>
+			</gml:exterior>
+		</gml:Polygon>
+	</xsl:template>
+
+	<!-- Prism with a Polygon and height -->
+	<xsl:template match="Prism">
+		<gs:Prism>
+			<xsl:call-template name="shape"/>
+			<gs:base>
+				<gml:Polygon>
+					<gml:exterior>
+						<gml:LinearRing>
+							<xsl:apply-templates select="./pos|posList"/>
+						</gml:LinearRing>
+					</gml:exterior>
+				</gml:Polygon>
+			</gs:base>
+			<xsl:apply-templates select="./height"/>
+		</gs:Prism>
+	</xsl:template>
+
+	<!-- method has no children so we add the "gp" namespace and copy in the value -->
+	<xsl:template match="method">
+		<gp:method>
+			 <xsl:value-of select="."/>
+		 </gp:method>
+	</xsl:template>
+
+	<!-- note-well has no children so we add the "gp" namespace and copy in the value -->
+	<xsl:template match="note-well">
+		<gp:note-well>
+			 <xsl:value-of select="."/>
+		 </gp:note-well>
+	</xsl:template>
+
+	<!-- usage-rules does have children so we add the "gp" namespace and copy in
+		the children, also adding the "gp" namespace -->
+	<xsl:template match="usage-rules">
+		<gp:usage-rules>
+			 <xsl:for-each select="*">
+				 <xsl:element name="gp:{local-name()}">
+					 <xsl:value-of select="."/>
+				 </xsl:element>
+			 </xsl:for-each>
+		</gp:usage-rules>
+	</xsl:template>
+
+	<!-- These are the GML format primitives -->
+
+	<xsl:template name="name-value">
+		<xsl:element name="gml:{name()}">
+			<xsl:value-of select="."/>
+		</xsl:element>
+	</xsl:template>
+
+	<xsl:template name="length">
+		<xsl:element name="gs:{name()}">
+			<xsl:attribute name="uom">urn:ogc:def:uom:EPSG::9001</xsl:attribute>
+			<xsl:value-of select="."/>
+		</xsl:element>
+	</xsl:template>
+
+	<xsl:template name="angle">
+		<xsl:element name="gs:{name()}">
+			<xsl:choose>
+				<xsl:when test="@uom = 'radians'">
+					<xsl:attribute name="uom">urn:ogc:def:uom:EPSG::9102</xsl:attribute>
+				</xsl:when>
+				<xsl:otherwise>
+					<xsl:attribute name="uom">urn:ogc:def:uom:EPSG::9101</xsl:attribute>
+				</xsl:otherwise>
+			</xsl:choose>
+			<xsl:value-of select="."/>
+		</xsl:element>
+	</xsl:template>
+
+	<!-- These are the GML shape parameters -->
+
+	<xsl:template match="orientation"><xsl:call-template name="angle" /></xsl:template>
+	<xsl:template match="radius"><xsl:call-template name="length" /></xsl:template>
+	<xsl:template match="height"><xsl:call-template name="length" /></xsl:template>
+	<xsl:template match="semiMajorAxis"><xsl:call-template name="length" /></xsl:template>
+	<xsl:template match="semiMinorAxis"><xsl:call-template name="length" /></xsl:template>
+	<xsl:template match="verticalAxis"><xsl:call-template name="length" /></xsl:template>
+	<xsl:template match="innerRadius"><xsl:call-template name="length" /></xsl:template>
+	<xsl:template match="outerRadius"><xsl:call-template name="length" /></xsl:template>
+	<xsl:template match="startAngle"><xsl:call-template name="angle" /></xsl:template>
+	<xsl:template match="openingAngle"><xsl:call-template name="angle" /></xsl:template>
+	<xsl:template match="pos"><xsl:call-template name="name-value" /></xsl:template>
+	<xsl:template match="posList"><xsl:call-template name="name-value" /></xsl:template>
+
+
+</xsl:stylesheet>
diff --git a/res/res_geolocation/geoloc_civicaddr.c b/res/res_geolocation/geoloc_civicaddr.c
new file mode 100644
index 0000000..f5a7c22
--- /dev/null
+++ b/res/res_geolocation/geoloc_civicaddr.c
@@ -0,0 +1,151 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2022, Sangoma Technologies Corporation
+ *
+ * George Joseph <gjoseph at sangoma.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+#include "asterisk.h"
+#include "asterisk/config.h"
+#include "asterisk/cli.h"
+#include "asterisk/res_geolocation.h"
+#include "asterisk/xml.h"
+#include "geoloc_private.h"
+
+static const char *addr_code_name_entries[] = {
+	"country",
+	"A1",
+	"A2",
+	"A3",
+	"A4",
+	"A5",
+	"A6",
+	"ADDCODE",
+	"BLD",
+	"FLR",
+	"HNO",
+	"HNS",
+	"LMK",
+	"LOC",
+	"NAM",
+	"PC",
+	"PCN",
+	"PLC",
+	"POBOX",
+	"POD",
+	"POM",
+	"PRD",
+	"PRM",
+	"RD",
+	"RD",
+	"RDBR",
+	"RDSEC",
+	"RDSUBBR",
+	"ROOM",
+	"SEAT",
+	"STS",
+	"UNIT",
+};
+
+static int compare_civicaddr_codes(const void *_a, const void *_b)
+{
+	/* See the man page for qsort(3) for an explanation of the casts */
+	int rc = strcmp(*(const char **)_a, *(const char **)_b);
+	return rc;
+}
+
+int ast_geoloc_civicaddr_is_code_valid(const char *code)
+{
+	const char **entry = bsearch(&code, addr_code_name_entries, ARRAY_LEN(addr_code_name_entries),
+		sizeof(const char *), compare_civicaddr_codes);
+	return (entry != NULL);
+}
+
+enum ast_geoloc_validate_result ast_geoloc_civicaddr_validate_varlist(
+	const struct ast_variable *varlist,	const char **result)
+{
+	const struct ast_variable *var = varlist;
+	for (; var; var = var->next) {
+		int valid = ast_geoloc_civicaddr_is_code_valid(var->name);
+		if (!valid) {
+			*result = var->name;
+			return AST_GEOLOC_VALIDATE_INVALID_VARNAME;
+		}
+	}
+	return AST_GEOLOC_VALIDATE_SUCCESS;
+}
+
+struct ast_xml_node *geoloc_civicaddr_list_to_xml(const struct ast_variable *resolved_location,
+	const char *ref_string)
+{
+	char *lang = NULL;
+	char *s = NULL;
+	struct ast_variable *var;
+	struct ast_xml_node *ca_node;
+	struct ast_xml_node *child_node;
+	int rc = 0;
+	SCOPE_ENTER(3, "%s", ref_string);
+
+	lang = (char *)ast_variable_find_in_list(resolved_location, "lang");
+	if (ast_strlen_zero(lang)) {
+		lang = ast_strdupa(ast_defaultlanguage);
+		for (s = lang; *s; s++) {
+			if (*s == '_') {
+				*s = '-';
+			}
+		}
+	}
+
+	ca_node = ast_xml_new_node("civicAddress");
+	if (!ca_node) {
+		SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create 'civicAddress' XML node\n", ref_string);
+	}
+	rc = ast_xml_set_attribute(ca_node, "lang", lang);
+	if (rc != 0) {
+		ast_xml_free_node(ca_node);
+		SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create 'lang' XML attribute\n", ref_string);
+	}
+
+	for (var = (struct ast_variable *)resolved_location; var; var = var->next) {
+		if (ast_strings_equal(var->name, "lang")) {
+			continue;
+		}
+		child_node = ast_xml_new_child(ca_node, var->name);
+		if (!child_node) {
+			ast_xml_free_node(ca_node);
+			SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create '%s' XML node\n", var->name, ref_string);
+		}
+		ast_xml_set_text(child_node, var->value);
+	}
+
+	SCOPE_EXIT_RTN_VALUE(ca_node, "%s: Done\n", ref_string);
+}
+
+int geoloc_civicaddr_unload(void)
+{
+	return AST_MODULE_LOAD_SUCCESS;
+}
+
+int geoloc_civicaddr_load(void)
+{
+	qsort(addr_code_name_entries, ARRAY_LEN(addr_code_name_entries), sizeof(const char *),
+		compare_civicaddr_codes);
+
+	return AST_MODULE_LOAD_SUCCESS;
+}
+
+int geoloc_civicaddr_reload(void)
+{
+	return AST_MODULE_LOAD_SUCCESS;
+}
diff --git a/res/res_geolocation/geoloc_common.c b/res/res_geolocation/geoloc_common.c
new file mode 100644
index 0000000..bb24a31
--- /dev/null
+++ b/res/res_geolocation/geoloc_common.c
@@ -0,0 +1,36 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2022, Sangoma Technologies Corporation
+ *
+ * George Joseph <gjoseph at sangoma.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+#include "asterisk.h"
+#include "geoloc_private.h"
+
+static const char *result_names[] = {
+	"Success",
+	"Missing type",
+	"Invalid shape type",
+	"Invalid variable name",
+	"Not enough variables",
+	"Too many variables",
+	"Invalid variable value"
+};
+
+const char *ast_geoloc_validate_result_to_str(enum ast_geoloc_validate_result result)
+{
+	return result_names[result];
+}
+
diff --git a/res/res_geolocation/geoloc_config.c b/res/res_geolocation/geoloc_config.c
new file mode 100644
index 0000000..33cd333
--- /dev/null
+++ b/res/res_geolocation/geoloc_config.c
@@ -0,0 +1,641 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2022, Sangoma Technologies Corporation
+ *
+ * George Joseph <gjoseph at sangoma.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+#include "asterisk.h"
+#include "asterisk/module.h"
+#include "asterisk/cli.h"
+#define AST_API_MODULE
+#include "geoloc_private.h"
+
+static struct ast_sorcery *geoloc_sorcery;
+
+static const char *pidf_element_names[] = {
+	"<none>",
+	"tuple",
+	"device",
+	"person"
+};
+
+static const char *format_names[] = {
+	"<none>",
+	"civicAddress",
+	"GML",
+	"URI",
+};
+
+static const char * action_names[] = {
+	"prefer_incoming",
+	"prefer_config",
+	"discard_incoming",
+	"discard_config",
+};
+
+CONFIG_ENUM(location, format)
+CONFIG_VAR_LIST(location, location_info)
+
+static void geoloc_location_destructor(void *obj) {
+	struct ast_geoloc_location *location = obj;
+
+	ast_string_field_free_memory(location);
+	ast_variables_destroy(location->location_info);
+}
+
+static void *geoloc_location_alloc(const char *name)
+{
+	struct ast_geoloc_location *location = ast_sorcery_generic_alloc(sizeof(struct ast_geoloc_location), geoloc_location_destructor);
+	if (location) {
+		ast_string_field_init(location, 128);
+	}
+
+	return location;
+}
+
+
+CONFIG_ENUM(profile, pidf_element)
+CONFIG_ENUM(profile, action)
+CONFIG_VAR_LIST(profile, location_refinement)
+CONFIG_VAR_LIST(profile, location_variables)
+CONFIG_VAR_LIST(profile, usage_rules)
+
+static void geoloc_profile_destructor(void *obj) {
+	struct ast_geoloc_profile *profile = obj;
+
+	ast_string_field_free_memory(profile);
+	ast_variables_destroy(profile->location_refinement);
+	ast_variables_destroy(profile->location_variables);
+	ast_variables_destroy(profile->usage_rules);
+}
+
+static void *geoloc_profile_alloc(const char *name)
+{
+	struct ast_geoloc_profile *profile = ast_sorcery_generic_alloc(sizeof(*profile), geoloc_profile_destructor);
+	if (profile) {
+		ast_string_field_init(profile, 128);
+	}
+
+	return profile;
+}
+
+static int geoloc_location_apply_handler(const struct ast_sorcery *sorcery, void *obj)
+{
+	struct ast_geoloc_location *location = obj;
+	const char *location_id = ast_sorcery_object_get_id(location);
+	const char *failed;
+	const char *uri;
+	enum ast_geoloc_validate_result result;
+
+	switch (location->format) {
+	case AST_GEOLOC_FORMAT_NONE:
+	case AST_GEOLOC_FORMAT_LAST:
+		ast_log(LOG_ERROR, "Location '%s' must have a format\n", location_id);
+		return -1;
+	case AST_GEOLOC_FORMAT_CIVIC_ADDRESS:
+		result = ast_geoloc_civicaddr_validate_varlist(location->location_info, &failed);
+		if (result != AST_GEOLOC_VALIDATE_SUCCESS) {
+			ast_log(LOG_ERROR, "Location '%s' has invalid item '%s' in the location\n",
+				location_id, failed);
+			return -1;
+		}
+		break;
+	case AST_GEOLOC_FORMAT_GML:
+		result = ast_geoloc_gml_validate_varlist(location->location_info, &failed);
+		if (result != AST_GEOLOC_VALIDATE_SUCCESS) {
+			ast_log(LOG_ERROR, "%s for item '%s' in location '%s'\n",
+				ast_geoloc_validate_result_to_str(result),	failed, location_id);
+			return -1;
+		}
+
+		break;
+	case AST_GEOLOC_FORMAT_URI:
+		uri = ast_variable_find_in_list(location->location_info, "URI");
+		if (!uri) {
+			struct ast_str *str = ast_variable_list_join(location->location_info, ",", "=", "\"", NULL);
+
+			ast_log(LOG_ERROR, "Geolocation location '%s' format is set to '%s' but no 'URI' was found in location parameter '%s'\n",
+				location_id, format_names[AST_GEOLOC_FORMAT_URI], ast_str_buffer(str));
+			ast_free(str);
+			return -1;
+		}
+		break;
+	}
+
+	if (!ast_strlen_zero(location->location_source)) {
+		struct ast_sockaddr loc_source_addr;
+		int rc = ast_sockaddr_parse(&loc_source_addr, location->location_source, PARSE_PORT_FORBID);
+		if (rc == 1) {
+			ast_log(LOG_ERROR, "Geolocation location '%s' location_source '%s' must be a FQDN."
+				" RFC8787 expressly forbids IP addresses.\n",
+				location_id, location->location_source);
+			return -1;
+		}
+	}
+
+
+	return 0;
+}
+
+static int geoloc_profile_apply_handler(const struct ast_sorcery *sorcery, void *obj)
+{
+	struct ast_geoloc_profile *profile = obj;
+	struct ast_geoloc_location *location;
+	const char *profile_id = ast_sorcery_object_get_id(profile);
+	const char *failed;
+	enum ast_geoloc_validate_result result;
+
+	if (ast_strlen_zero(profile->location_reference)) {
+		if (profile->location_refinement ||
+			profile->location_variables) {
+			ast_log(LOG_ERROR, "Profile '%s' can't have location_refinement or location_variables without a location_reference",
+				profile_id);
+			return -1;
+		}
+		return 0;
+	}
+
+	location = ast_sorcery_retrieve_by_id(geoloc_sorcery, "location", profile->location_reference);
+	if (!location) {
+		ast_log(LOG_ERROR, "Profile '%s' has a location_reference '%s' that doesn't exist",
+			profile_id, profile->location_reference);
+		return -1;
+	}
+
+	if (profile->location_refinement) {
+		switch (location->format) {
+		case AST_GEOLOC_FORMAT_NONE:
+		case AST_GEOLOC_FORMAT_LAST:
+			break;
+		case AST_GEOLOC_FORMAT_CIVIC_ADDRESS:
+			result = ast_geoloc_civicaddr_validate_varlist(profile->location_refinement, &failed);
+			if (result != AST_GEOLOC_VALIDATE_SUCCESS) {
+				ast_log(LOG_ERROR, "Profile '%s' error: %s: for item '%s' in the location_refinement\n",
+					profile_id,	ast_geoloc_validate_result_to_str(result), failed);
+				ao2_ref(location, -1);
+				return -1;
+			}
+			break;
+		case AST_GEOLOC_FORMAT_GML:
+			break;
+		case AST_GEOLOC_FORMAT_URI:
+			break;
+		}
+	}
+	ao2_ref(location, -1);
+
+	return 0;
+}
+
+struct ast_sorcery *geoloc_get_sorcery(void)
+{
+	ast_sorcery_ref(geoloc_sorcery);
+	return geoloc_sorcery;
+}
+
+static char *geoloc_config_list_locations(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
+{
+	struct ao2_iterator iter;
+	struct ao2_container *sorted_container;
+	struct ao2_container *unsorted_container;
+	struct ast_geoloc_location *loc;
+	int using_regex = 0;
+	char *result = CLI_SUCCESS;
+	int ret = 0;
+	char *format_name;
+	int count = 0;
+
+	switch (cmd) {
+	case CLI_INIT:
+		e->command = "geoloc list locations";
+		e->usage = "Usage: geoloc list locations [ like <pattern> ]\n"
+		            "      List Geolocation Location Objects\n";
+		return NULL;
+	case CLI_GENERATE:
+		return NULL;
+	}
+
+	if (a->argc != 3 && a->argc != 5) {
+		return CLI_SHOWUSAGE;
+	}
+
+	if (a->argc == 5) {
+		if (strcasecmp(a->argv[3], "like")) {
+			return CLI_SHOWUSAGE;
+		}
+		using_regex = 1;
+	}
+
+	sorted_container = ao2_container_alloc_rbtree(AO2_ALLOC_OPT_LOCK_NOLOCK, 0,
+		ast_sorcery_object_id_sort, NULL);
+	if (!sorted_container) {
+		ast_cli(a->fd, "Geolocation Location Objects: Unable to allocate temporary container\n");
+		return CLI_FAILURE;
+	}
+
+	/* Get a sorted snapshot of the scheduled tasks */
+	if (using_regex) {
+		unsorted_container = ast_sorcery_retrieve_by_regex(geoloc_sorcery, "location", a->argv[4]);
+	} else {
+		unsorted_container = ast_sorcery_retrieve_by_fields(geoloc_sorcery, "location",
+			AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL);
+	}
+
+	ret = ao2_container_dup(sorted_container, unsorted_container, 0);
+	ao2_ref(unsorted_container, -1);
+	if (ret != 0) {
+		ao2_ref(sorted_container, -1);
+		ast_cli(a->fd, "Geolocation Location Objects: Unable to sort temporary container\n");
+		return CLI_FAILURE;
+	}
+
+	ast_cli(a->fd, "Geolocation Location Objects:\n\n");
+
+	ast_cli(a->fd,
+		"<Object ID...................................> <Format.....> <Details.............>\n"
+		"===================================================================================\n");
+
+	iter = ao2_iterator_init(sorted_container, AO2_ITERATOR_UNLINK);
+	for (; (loc = ao2_iterator_next(&iter)); ao2_ref(loc, -1)) {
+		struct ast_str *str;
+
+		ao2_lock(loc);
+		str = ast_variable_list_join(loc->location_info, ",", "=", "\"", NULL);
+		if (!str) {
+			ao2_unlock(loc);
+			ao2_ref(loc, -1);
+			ast_cli(a->fd, "Geolocation Location Objects: Unable to allocate temp string for '%s'\n",
+				ast_sorcery_object_get_id(loc));
+			result = CLI_FAILURE;
+			break;
+		}
+
+		format_to_str(loc, NULL, &format_name);
+		ast_cli(a->fd, "%-46.46s %-13s %-s\n",
+			ast_sorcery_object_get_id(loc),
+			format_name,
+			ast_str_buffer(str));
+		ao2_unlock(loc);
+		ast_free(str);
+		ast_free(format_name);
+		count++;
+	}
+	ao2_iterator_destroy(&iter);
+	ao2_ref(sorted_container, -1);
+	ast_cli(a->fd, "\nTotal Location Objects: %d\n\n", count);
+
+	return result;
+}
+
+static char *geoloc_config_list_profiles(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
+{
+	struct ao2_iterator iter;
+	struct ao2_container *sorted_container;
+	struct ao2_container *unsorted_container;
+	struct ast_geoloc_profile *profile;
+	int using_regex = 0;
+	char *result = CLI_SUCCESS;
+	int ret = 0;
+	char *action;
+	int count = 0;
+
+	switch (cmd) {
+	case CLI_INIT:
+		e->command = "geoloc list profiles";
+		e->usage = "Usage: geoloc list profiles [ like <pattern> ]\n"
+		            "      List Geolocation Profile Objects\n";
+		return NULL;
+	case CLI_GENERATE:
+		return NULL;
+	}
+
+	if (a->argc != 3 && a->argc != 5) {
+		return CLI_SHOWUSAGE;
+	}
+
+	if (a->argc == 5) {
+		if (strcasecmp(a->argv[3], "like")) {
+			return CLI_SHOWUSAGE;
+		}
+		using_regex = 1;
+	}
+
+	sorted_container = ao2_container_alloc_rbtree(AO2_ALLOC_OPT_LOCK_NOLOCK, 0,
+		ast_sorcery_object_id_sort, NULL);
+	if (!sorted_container) {
+		ast_cli(a->fd, "Geolocation Profile Objects: Unable to allocate temporary container\n");
+		return CLI_FAILURE;
+	}
+
+	/* Get a sorted snapshot of the scheduled tasks */
+	if (using_regex) {
+		unsorted_container = ast_sorcery_retrieve_by_regex(geoloc_sorcery, "profile", a->argv[4]);
+	} else {
+		unsorted_container = ast_sorcery_retrieve_by_fields(geoloc_sorcery, "profile",
+			AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL);
+	}
+
+	ret = ao2_container_dup(sorted_container, unsorted_container, 0);
+	ao2_ref(unsorted_container, -1);
+	if (ret != 0) {
+		ao2_ref(sorted_container, -1);
+		ast_cli(a->fd, "Geolocation Profile Objects: Unable to sort temporary container\n");
+		return CLI_FAILURE;
+	}
+
+	ast_cli(a->fd, "Geolocation Profile Objects:\n\n");
+
+	ast_cli(a->fd,
+		"<Object ID...................................> <Profile Action> <Location Reference> \n"
+		"=====================================================================================\n");
+
+	iter = ao2_iterator_init(sorted_container, AO2_ITERATOR_UNLINK);
+	for (; (profile = ao2_iterator_next(&iter)); ao2_ref(profile, -1)) {
+		ao2_lock(profile);
+
+		action_to_str(profile, NULL, &action);
+		ast_cli(a->fd, "%-46.46s %-16s %-s\n",
+			ast_sorcery_object_get_id(profile),
+			action,
+			profile->location_reference);
+		ao2_unlock(profile);
+		ast_free(action);
+		count++;
+	}
+	ao2_iterator_destroy(&iter);
+	ao2_ref(sorted_container, -1);
+	ast_cli(a->fd, "\nTotal Profile Objects: %d\n\n", count);
+
+	return result;
+}
+
+static char *geoloc_config_show_profiles(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
+{
+	struct ao2_iterator iter;
+	struct ao2_container *sorted_container;
+	struct ao2_container *unsorted_container;
+	struct ast_geoloc_profile *profile;
+	int using_regex = 0;
+	char *result = CLI_SUCCESS;
+	int ret = 0;
+	int count = 0;
+
+	switch (cmd) {
+	case CLI_INIT:
+		e->command = "geoloc show profiles";
+		e->usage = "Usage: geoloc show profiles [ like <pattern> ]\n"
+		            "      List Geolocation Profile Objects\n";
+		return NULL;
+	case CLI_GENERATE:
+		return NULL;
+	}
+
+	if (a->argc != 3 && a->argc != 5) {
+		return CLI_SHOWUSAGE;
+	}
+
+	if (a->argc == 5) {
+		if (strcasecmp(a->argv[3], "like")) {
+			return CLI_SHOWUSAGE;
+		}
+		using_regex = 1;
+	}
+
+	/* Create an empty rb-tree container which always sorts its contents. */
+	sorted_container = ao2_container_alloc_rbtree(AO2_ALLOC_OPT_LOCK_NOLOCK, 0,
+		ast_sorcery_object_id_sort, NULL);
+	if (!sorted_container) {
+		ast_cli(a->fd, "Geolocation Profile Objects: Unable to allocate temporary container\n");
+		return CLI_FAILURE;
+	}
+
+	/* Get an unsorted list of profile parameters */
+	if (using_regex) {
+		unsorted_container = ast_sorcery_retrieve_by_regex(geoloc_sorcery, "profile", a->argv[4]);
+	} else {
+		unsorted_container = ast_sorcery_retrieve_by_fields(geoloc_sorcery, "profile",
+			AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL);
+	}
+
+	/* Copy the unsorted parameters into the rb-tree container which will sort them automatically. */
+	ret = ao2_container_dup(sorted_container, unsorted_container, 0);
+	ao2_ref(unsorted_container, -1);
+	if (ret != 0) {
+		ao2_ref(sorted_container, -1);
+		ast_cli(a->fd, "Geolocation Profile Objects: Unable to sort temporary container\n");
+		return CLI_FAILURE;
+	}
+
+	ast_cli(a->fd, "Geolocation Profile Objects:\n\n");
+
+	iter = ao2_iterator_init(sorted_container, AO2_ITERATOR_UNLINK);
+	for (; (profile = ao2_iterator_next(&iter)); ) {
+		char *action = NULL;
+		struct ast_str *loc_str = NULL;
+		struct ast_str *refinement_str = NULL;
+		struct ast_str *variables_str = NULL;
+		struct ast_str *resolved_str = NULL;
+		struct ast_str *usage_rules_str = NULL;
+		struct ast_geoloc_eprofile *eprofile = ast_geoloc_eprofile_create_from_profile(profile);
+		ao2_ref(profile, -1);
+
+		if (!ast_strlen_zero(eprofile->location_reference)) {
+			loc_str = ast_variable_list_join(eprofile->location_info, ",", "=", "\"", NULL);
+			resolved_str = ast_variable_list_join(eprofile->effective_location, ",", "=", "\"", NULL);
+		}
+
+		refinement_str = ast_variable_list_join(eprofile->location_refinement, ",", "=", "\"", NULL);
+		variables_str = ast_variable_list_join(eprofile->location_variables, ",", "=", "\"", NULL);
+		usage_rules_str = ast_variable_list_join(eprofile->usage_rules, ",", "=", "\"", NULL);
+
+		action_to_str(eprofile, NULL, &action);
+
+		ast_cli(a->fd,
+			"id:                   %-s\n"
+			"profile_action:       %-s\n"
+			"pidf_element:         %-s\n"
+			"location_reference:   %-s\n"
+			"Location_format:      %-s\n"
+			"location_details:     %-s\n"
+			"location_method:      %-s\n"
+			"location_refinement:  %-s\n"
+			"location_variables:   %-s\n"
+			"effective_location:   %-s\n"
+			"usage_rules:          %-s\n"
+			"notes:                %-s\n",
+			eprofile->id,
+			action,
+			pidf_element_names[eprofile->pidf_element],
+			S_OR(eprofile->location_reference, "<none>"),
+			format_names[eprofile->format],
+			S_COR(loc_str, ast_str_buffer(loc_str), "<none>"),
+			S_OR(eprofile->method, "<none>"),
+			S_COR(refinement_str, ast_str_buffer(refinement_str), "<none>"),
+			S_COR(variables_str, ast_str_buffer(variables_str), "<none>"),
+			S_COR(resolved_str, ast_str_buffer(resolved_str), "<none>"),
+			S_COR(usage_rules_str, ast_str_buffer(usage_rules_str), "<none>"),
+			S_OR(eprofile->notes, "<none>")
+			);
+		ao2_ref(eprofile, -1);
+
+		ast_free(action);
+		ast_free(loc_str);
+		ast_free(refinement_str);
+		ast_free(variables_str);
+		ast_free(resolved_str);
+		ast_free(usage_rules_str);
+		count++;
+	}
+	ao2_iterator_destroy(&iter);
+	ao2_ref(sorted_container, -1);
+	ast_cli(a->fd, "\nTotal Profile Objects: %d\n\n", count);
+
+	return result;
+}
+
+static char *geoloc_config_cli_reload(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
+{
+	char *result = CLI_SUCCESS;
+
+	switch (cmd) {
+	case CLI_INIT:
+		e->command = "geoloc reload";
+		e->usage = "Usage: geoloc reload\n"
+		            "      Reload Geolocation Configuration\n";
+		return NULL;
+	case CLI_GENERATE:
+		return NULL;
+	}
+
+	if (a->argc != 2) {
+		return CLI_SHOWUSAGE;
+	}
+
+	geoloc_config_reload();
+	ast_cli(a->fd, "Geolocation Configuration reloaded.\n");
+
+	return result;
+}
+
+static struct ast_cli_entry geoloc_location_cli_commands[] = {
+	AST_CLI_DEFINE(geoloc_config_list_locations, "List Geolocation Location Objects"),
+	AST_CLI_DEFINE(geoloc_config_list_profiles, "List Geolocation Profile Objects"),
+	AST_CLI_DEFINE(geoloc_config_show_profiles, "Show Geolocation Profile Objects"),
+	AST_CLI_DEFINE(geoloc_config_cli_reload, "Reload Geolocation Configuration"),
+};
+
+struct ast_geoloc_location * AST_OPTIONAL_API_NAME(ast_geoloc_get_location)(const char *id)
+{
+	if (ast_strlen_zero(id)) {
+		return NULL;
+	}
+
+	return ast_sorcery_retrieve_by_id(geoloc_sorcery, "location", id);
+}
+
+struct ast_geoloc_profile * AST_OPTIONAL_API_NAME(ast_geoloc_get_profile)(const char *id)
+{
+	if (ast_strlen_zero(id)) {
+		return NULL;
+	}
+
+	return ast_sorcery_retrieve_by_id(geoloc_sorcery, "profile", id);
+}
+
+int geoloc_config_reload(void)
+{
+	if (geoloc_sorcery) {
+		ast_sorcery_reload(geoloc_sorcery);
+	}
+	return AST_MODULE_LOAD_SUCCESS;
+}
+
+int geoloc_config_unload(void)
+{
+	ast_cli_unregister_multiple(geoloc_location_cli_commands, ARRAY_LEN(geoloc_location_cli_commands));
+
+	ast_sorcery_object_unregister(geoloc_sorcery, "profile");
+	ast_sorcery_object_unregister(geoloc_sorcery, "location");
+
+	if (geoloc_sorcery) {
+		ast_sorcery_unref(geoloc_sorcery);
+	}
+	geoloc_sorcery = NULL;
+
+	return 0;
+}
+
+int geoloc_config_load(void)
+{
+	if (!(geoloc_sorcery = ast_sorcery_open())) {
+		ast_log(LOG_ERROR, "Failed to open geolocation sorcery\n");
+		return AST_MODULE_LOAD_DECLINE;
+	}
+
+	ast_sorcery_apply_default(geoloc_sorcery, "location", "config", "geolocation.conf,criteria=type=location");
+	if (ast_sorcery_object_register(geoloc_sorcery, "location", geoloc_location_alloc, NULL, geoloc_location_apply_handler)) {
+		ast_log(LOG_ERROR, "Failed to register geoloc location object with sorcery\n");
+		ast_sorcery_unref(geoloc_sorcery);
+		geoloc_sorcery = NULL;
+		return AST_MODULE_LOAD_DECLINE;
+	}
+
+	ast_sorcery_object_field_register(geoloc_sorcery, "location", "type", "", OPT_NOOP_T, 0, 0);
+	ast_sorcery_object_field_register_custom(geoloc_sorcery, "location", "format", AST_GEOLOC_FORMAT_NONE,
+		format_handler, format_to_str, NULL, 0, 0);
+	ast_sorcery_object_field_register_custom(geoloc_sorcery, "location", "location_info", NULL,
+		location_info_handler, location_info_to_str, location_info_dup, 0, 0);
+	ast_sorcery_object_field_register(geoloc_sorcery, "location", "location_source", "", OPT_STRINGFIELD_T,
+		0, STRFLDSET(struct ast_geoloc_location, location_source));
+	ast_sorcery_object_field_register(geoloc_sorcery, "location", "method", "", OPT_STRINGFIELD_T,
+		0, STRFLDSET(struct ast_geoloc_location, method));
+
+
+	ast_sorcery_apply_default(geoloc_sorcery, "profile", "config", "geolocation.conf,criteria=type=profile");
+	if (ast_sorcery_object_register(geoloc_sorcery, "profile", geoloc_profile_alloc, NULL, geoloc_profile_apply_handler)) {
+		ast_log(LOG_ERROR, "Failed to register geoloc profile object with sorcery\n");
+		ast_sorcery_unref(geoloc_sorcery);
+		geoloc_sorcery = NULL;
+		return AST_MODULE_LOAD_DECLINE;
+	}
+
+	ast_sorcery_object_field_register(geoloc_sorcery, "profile", "type", "", OPT_NOOP_T, 0, 0);
+	ast_sorcery_object_field_register_custom(geoloc_sorcery, "profile", "pidf_element",
+		pidf_element_names[AST_PIDF_ELEMENT_DEVICE], pidf_element_handler, pidf_element_to_str, NULL, 0, 0);
+	ast_sorcery_object_field_register(geoloc_sorcery, "profile", "location_reference", "", OPT_STRINGFIELD_T,
+		0, STRFLDSET(struct ast_geoloc_profile, location_reference));
+	ast_sorcery_object_field_register_custom(geoloc_sorcery, "profile", "profile_action", "discard_incoming",
+		action_handler, action_to_str, NULL, 0, 0);
+	ast_sorcery_object_field_register_custom(geoloc_sorcery, "profile", "usage_rules", NULL,
+		usage_rules_handler, usage_rules_to_str, usage_rules_dup, 0, 0);
+	ast_sorcery_object_field_register_custom(geoloc_sorcery, "profile", "location_info_refinement", NULL,
+		location_refinement_handler, location_refinement_to_str, location_refinement_dup, 0, 0);
+	ast_sorcery_object_field_register_custom(geoloc_sorcery, "profile", "location_variables", NULL,
+		location_variables_handler, location_variables_to_str, location_variables_dup, 0, 0);
+	ast_sorcery_object_field_register(geoloc_sorcery, "profile", "notes", "", OPT_STRINGFIELD_T,
+		0, STRFLDSET(struct ast_geoloc_profile, notes));
+
+	ast_sorcery_load(geoloc_sorcery);
+
+	ast_cli_register_multiple(geoloc_location_cli_commands, ARRAY_LEN(geoloc_location_cli_commands));
+
+	return AST_MODULE_LOAD_SUCCESS;
+}
+
+int AST_OPTIONAL_API_NAME(ast_geoloc_is_loaded)(void)
+{
+	return 1;
+}
+
diff --git a/res/res_geolocation/geoloc_datastore.c b/res/res_geolocation/geoloc_datastore.c
new file mode 100644
index 0000000..040a9bd
--- /dev/null
+++ b/res/res_geolocation/geoloc_datastore.c
@@ -0,0 +1,325 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2022, Sangoma Technologies Corporation
+ *
+ * George Joseph <gjoseph at sangoma.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+#include "asterisk.h"
+#include "asterisk/astobj2.h"
+#include "asterisk/datastore.h"
+#include "asterisk/channel.h"
+#include "asterisk/res_geolocation.h"
+#include "asterisk/vector.h"
+#include "geoloc_private.h"
+
+#define GEOLOC_DS_TYPE "geoloc_eprofiles"
+
+struct ast_sorcery *geoloc_sorcery;
+
+struct eprofiles_datastore {
+	const char *id;
+	AST_VECTOR(geoloc_eprofiles, struct ast_geoloc_eprofile *) eprofiles;
+};
+
+static void geoloc_datastore_free(void *obj)
+{
+	struct eprofiles_datastore *eds = obj;
+
+	AST_VECTOR_RESET(&eds->eprofiles, ao2_cleanup);
+	AST_VECTOR_FREE(&eds->eprofiles);
+	ast_free(eds);
+}
+
+static void *geoloc_datastore_duplicate(void *obj)
+{
+	struct eprofiles_datastore *in_eds = obj;
+	struct eprofiles_datastore *out_eds;
+	int rc = 0;
+	int i = 0;
+	int eprofile_count = 0;
+
+	out_eds = ast_calloc(1, sizeof(*out_eds));
+	if (!out_eds) {
+		return NULL;
+	}
+
+	rc = AST_VECTOR_INIT(&out_eds->eprofiles, 2);
+	if (rc != 0) {
+		ast_free(out_eds);
+		return NULL;
+	}
+
+	eprofile_count = AST_VECTOR_SIZE(&in_eds->eprofiles);
+	for (i = 0; i < eprofile_count; i++) {
+		struct ast_geoloc_eprofile *ep = AST_VECTOR_GET(&in_eds->eprofiles, i);
+		rc = AST_VECTOR_APPEND(&out_eds->eprofiles, ao2_bump(ep));
+		if (rc != 0) {
+			/* This will clean up the bumped reference to the eprofile */
+			geoloc_datastore_free(out_eds);
+			return NULL;
+		}
+	}
+
+	return out_eds;
+}
+
+static const struct ast_datastore_info geoloc_datastore_info = {
+	.type = GEOLOC_DS_TYPE,
+	.destroy = geoloc_datastore_free,
+	.duplicate = geoloc_datastore_duplicate,
+};
+
+#define IS_GEOLOC_DS(_ds) (_ds && _ds->data && ast_strings_equal(_ds->info->type, GEOLOC_DS_TYPE))
+
+const char *ast_geoloc_datastore_get_id(struct ast_datastore *ds)
+{
+	struct eprofiles_datastore *eds = NULL;
+
+	if (!IS_GEOLOC_DS(ds)) {
+		return NULL;
+	}
+
+	eds = (struct eprofiles_datastore *)ds->data;
+
+	return eds->id;
+}
+
+struct ast_datastore *ast_geoloc_datastore_create(const char *id)
+{
+	struct ast_datastore *ds = NULL;
+	struct eprofiles_datastore *eds = NULL;
+	int rc = 0;
+
+	if (ast_strlen_zero(id)) {
+		ast_log(LOG_ERROR, "A geoloc datastore can't be allocated with a NULL or empty id\n");
+		return NULL;
+	}
+
+	ds = ast_datastore_alloc(&geoloc_datastore_info, NULL);
+	if (!ds) {
+		ast_log(LOG_ERROR, "Geoloc datastore '%s' couldn't be allocated\n", id);
+		return NULL;
+	}
+
+	eds = ast_calloc(1, sizeof(*eds));
+	if (!eds) {
+		ast_datastore_free(ds);
+		ast_log(LOG_ERROR, "Private structure for geoloc datastore '%s' couldn't be allocated\n", id);
+		return NULL;
+	}
+	ds->data = eds;
+
+
+	rc = AST_VECTOR_INIT(&eds->eprofiles, 2);
+	if (rc != 0) {
+		ast_datastore_free(ds);
+		ast_log(LOG_ERROR, "Vector for geoloc datastore '%s' couldn't be initialized\n", id);
+		return NULL;
+	}
+
+	return ds;
+}
+
+int ast_geoloc_datastore_add_eprofile(struct ast_datastore *ds,
+	struct ast_geoloc_eprofile *eprofile)
+{
+	struct eprofiles_datastore *eds = NULL;
+	int rc = 0;
+
+	if (!IS_GEOLOC_DS(ds) || !eprofile) {
+		return -1;
+	}
+
+	eds = ds->data;
+	rc = AST_VECTOR_APPEND(&eds->eprofiles, ao2_bump(eprofile));
+	if (rc != 0) {
+		ao2_ref(eprofile, -1);
+		ast_log(LOG_ERROR, "Couldn't add eprofile '%s' to geoloc datastore '%s'\n", eprofile->id, eds->id);
+		return -1;
+	}
+
+	return AST_VECTOR_SIZE(&eds->eprofiles);
+}
+
+int ast_geoloc_datastore_insert_eprofile(struct ast_datastore *ds,
+	struct ast_geoloc_eprofile *eprofile, int index)
+{
+	struct eprofiles_datastore *eds = NULL;
+	int rc = 0;
+
+	if (!IS_GEOLOC_DS(ds) || !eprofile) {
+		return -1;
+	}
+
+	eds = ds->data;
+	rc = AST_VECTOR_INSERT_AT(&eds->eprofiles, index, ao2_bump(eprofile));
+	if (rc != 0) {
+		ao2_ref(eprofile, -1);
+		ast_log(LOG_ERROR, "Couldn't add eprofile '%s' to geoloc datastore '%s' in position '%d'\n",
+			eprofile->id, eds->id, index);
+		return -1;
+	}
+
+	return AST_VECTOR_SIZE(&eds->eprofiles);
+}
+
+int ast_geoloc_datastore_size(struct ast_datastore *ds)
+{
+	struct eprofiles_datastore *eds = NULL;
+
+	if (!IS_GEOLOC_DS(ds)) {
+		return -1;
+	}
+
+	eds = ds->data;
+
+	return AST_VECTOR_SIZE(&eds->eprofiles);
+}
+
+int ast_geoloc_datastore_set_inheritance(struct ast_datastore *ds, int inherit)
+{
+	if (!IS_GEOLOC_DS(ds)) {
+		return -1;
+	}
+	ds->inheritance = inherit ? DATASTORE_INHERIT_FOREVER : 0;
+	return 0;
+}
+
+struct ast_geoloc_eprofile *ast_geoloc_datastore_get_eprofile(struct ast_datastore *ds, int ix)
+{
+	struct eprofiles_datastore *eds = NULL;
+	struct ast_geoloc_eprofile *eprofile;
+
+	if (!IS_GEOLOC_DS(ds)) {
+		return NULL;
+	}
+
+	eds = ds->data;
+
+	if (ix >= AST_VECTOR_SIZE(&eds->eprofiles)) {
+		return NULL;
+	}
+
+	eprofile  = AST_VECTOR_GET(&eds->eprofiles, ix);
+	return ao2_bump(eprofile);
+}
+
+struct ast_datastore *ast_geoloc_datastore_find(struct ast_channel *chan)
+{
+	return ast_channel_datastore_find(chan, &geoloc_datastore_info, NULL);
+}
+
+int ast_geoloc_datastore_delete_eprofile(struct ast_datastore *ds, int ix)
+{
+	struct eprofiles_datastore *eds = NULL;
+
+	if (!IS_GEOLOC_DS(ds)) {
+		return -1;
+	}
+
+	eds = ds->data;
+
+	if (ix >= AST_VECTOR_SIZE(&eds->eprofiles)) {
+		return -1;
+	}
+
+	ao2_ref(AST_VECTOR_REMOVE(&eds->eprofiles, ix, 1), -1);
+	return 0;
+}
+
+struct ast_datastore *ast_geoloc_datastore_create_from_eprofile(
+	struct ast_geoloc_eprofile *eprofile)
+{
+	struct ast_datastore *ds;
+	int rc = 0;
+
+	if (!eprofile) {
+		return NULL;
+	}
+
+	ds = ast_geoloc_datastore_create(eprofile->id);
+	if (!ds) {
+		return NULL;
+	}
+
+	rc = ast_geoloc_datastore_add_eprofile(ds, eprofile);
+	if (rc != 0) {
+		ast_datastore_free(ds);
+		ds = NULL;
+	}
+
+	return ds;
+}
+
+struct ast_datastore *ast_geoloc_datastore_create_from_profile_name(const char *profile_name)
+{
+	struct ast_datastore *ds = NULL;
+	struct ast_geoloc_eprofile *eprofile = NULL;
+	struct ast_geoloc_profile *profile = NULL;
+	int rc = 0;
+
+	if (ast_strlen_zero(profile_name)) {
+		return NULL;
+	}
+
+	profile = ast_sorcery_retrieve_by_id(geoloc_sorcery, "profile", profile_name);
+	if (!profile) {
+		ast_log(LOG_ERROR, "A profile with the name '%s' was not found\n", profile_name);
+		return NULL;
+	}
+
+	ds = ast_geoloc_datastore_create(profile_name);
+	if (!ds) {
+		ast_log(LOG_ERROR, "A datastore couldn't be allocated for profile '%s'\n", profile_name);
+		ao2_ref(profile, -1);
+		return NULL;
+	}
+
+	eprofile = ast_geoloc_eprofile_create_from_profile(profile);
+	ao2_ref(profile, -1);
+	if (!eprofile) {
+		ast_datastore_free(ds);
+		ast_log(LOG_ERROR, "An effective profile with the name '%s' couldn't be allocated\n", profile_name);
+		return NULL;
+	}
+
+	rc = ast_geoloc_datastore_add_eprofile(ds, eprofile);
+	ao2_ref(eprofile, -1);
+	if (rc != 0) {
+		ast_datastore_free(ds);
+		ds = NULL;
+	}
+
+	return ds;
+}
+
+int geoloc_channel_unload(void)
+{
+	if (geoloc_sorcery) {
+		ast_sorcery_unref(geoloc_sorcery);
+	}
+	return AST_MODULE_LOAD_SUCCESS;
+}
+
+int geoloc_channel_load(void)
+{
+	geoloc_sorcery = geoloc_get_sorcery();
+	return AST_MODULE_LOAD_SUCCESS;
+}
+
+int geoloc_channel_reload(void)
+{
+	return AST_MODULE_LOAD_SUCCESS;
+}
diff --git a/res/res_geolocation/geoloc_dialplan.c b/res/res_geolocation/geoloc_dialplan.c
new file mode 100644
index 0000000..efa234a
--- /dev/null
+++ b/res/res_geolocation/geoloc_dialplan.c
@@ -0,0 +1,457 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2022, Sangoma Technologies Corporation
+ *
+ * George Joseph <gjoseph at sangoma.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+#include "asterisk.h"
+#include "asterisk/config.h"
+#include "asterisk/cli.h"
+#include "asterisk/module.h"
+#include "asterisk/channel.h"
+#include "asterisk/pbx.h"
+#include "asterisk/strings.h"
+#include "asterisk/utils.h"
+#include "asterisk/app.h"
+#include "asterisk/res_geolocation.h"
+#include "geoloc_private.h"
+
+static void varlist_to_str(struct ast_variable *list, struct ast_str** buf, size_t len)
+{
+	struct ast_variable *var = list;
+
+	for (; var; var = var->next) {
+		ast_str_append(buf, len, "%s=\"%s\"%s", var->name, var->value, var->next ? "," : "");
+	}
+}
+
+static int geoloc_profile_read(struct ast_channel *chan,
+	const char *cmd, char *data, struct ast_str **buf, ssize_t len)
+{
+	char *parsed_data = ast_strdupa(data);
+	int index = -1;
+	struct ast_datastore *ds;
+	struct ast_geoloc_eprofile *eprofile = NULL;
+	int profile_count = 0;
+
+	AST_DECLARE_APP_ARGS(args,
+		AST_APP_ARG(field);
+		AST_APP_ARG(index);
+	);
+
+	/* Check for zero arguments */
+	if (ast_strlen_zero(parsed_data)) {
+		ast_log(LOG_ERROR, "%s: Cannot call without arguments\n", cmd);
+		return -1;
+	}
+
+	AST_STANDARD_APP_ARGS(args, parsed_data);
+
+	if (ast_strlen_zero(args.field)) {
+		ast_log(LOG_ERROR, "%s: Cannot call without a field to query\n", cmd);
+		return -1;
+	}
+
+	if (!ast_strlen_zero(args.index)) {
+		if (sscanf(args.index, "%30d", &index) != 1) {
+			ast_log(LOG_ERROR, "%s: profile_index '%s' is invalid\n", cmd, args.index);
+			return -1;
+		}
+	}
+
+	ds = ast_geoloc_datastore_find(chan);
+	if (!ds) {
+		ast_log(LOG_NOTICE, "%s: There are no geoloc profiles on this channel\n", cmd);
+		return -1;
+	}
+
+	profile_count = ast_geoloc_datastore_size(ds);
+
+	if (index < 0) {
+		if (ast_strings_equal(args.field, "count")) {
+			ast_str_append(buf, len, "%d", profile_count);
+		} else if (ast_strings_equal(args.field, "inheritable")) {
+			ast_str_append(buf, len, "%d", ds->inheritance ? 1 : 0);
+		} else {
+			ast_log(LOG_ERROR, "%s: Field '%s' is not valid\n", cmd, args.field);
+			return -1;
+		}
+
+		return 0;
+	}
+
+	if (index >= profile_count) {
+		ast_log(LOG_ERROR, "%s: index %d is out of range 0 -> %d\n", cmd, index, profile_count);
+		return -1;
+	}
+
+	eprofile = ast_geoloc_datastore_get_eprofile(ds, index);
+	if (!eprofile) {
+		ast_log(LOG_ERROR, "%s: Internal Error.  Profile at index %d couldn't be retrieved.\n", cmd, index);
+		return -1;
+	}
+
+	if (ast_strings_equal(args.field, "id")) {
+		ast_str_append(buf, len, "%s", eprofile->id);
+	} else if (ast_strings_equal(args.field, "location_reference")) {
+		ast_str_append(buf, len, "%s", eprofile->location_reference);
+	} else if (ast_strings_equal(args.field, "method")) {
+		ast_str_append(buf, len, "%s", eprofile->method);
+	} else if (ast_strings_equal(args.field, "geolocation_routing")) {
+		ast_str_append(buf, len, "%s", eprofile->geolocation_routing ? "yes" : "no");
+	} else if (ast_strings_equal(args.field, "profile_action")) {
+		ast_str_append(buf, len, "%s", geoloc_action_to_name(eprofile->action));
+	} else if (ast_strings_equal(args.field, "format")) {
+		ast_str_append(buf, len, "%s", geoloc_format_to_name(eprofile->format));
+	} else if (ast_strings_equal(args.field, "pidf_element")) {
+		ast_str_append(buf, len, "%s", geoloc_pidf_element_to_name(eprofile->pidf_element));
+	} else if (ast_strings_equal(args.field, "location_source")) {
+		ast_str_append(buf, len, "%s", eprofile->location_source);
+	} else if (ast_strings_equal(args.field, "location_info")) {
+		varlist_to_str(eprofile->location_info, buf, len);
+	} else if (ast_strings_equal(args.field, "location_info_refinement")) {
+		varlist_to_str(eprofile->location_refinement, buf, len);
+	} else if (ast_strings_equal(args.field, "location_variables")) {
+		varlist_to_str(eprofile->location_variables, buf, len);
+	} else if (ast_strings_equal(args.field, "effective_location")) {
+		varlist_to_str(eprofile->effective_location, buf, len);
+	} else if (ast_strings_equal(args.field, "usage_rules")) {
+		varlist_to_str(eprofile->usage_rules, buf, len);
+	} else {
+		ast_log(LOG_ERROR, "%s: Field '%s' is not valid\n", cmd, args.field);
+		return -1;
+	}
+
+	ao2_ref(eprofile, -1);
+	return 0;
+}
+
+#define TEST_ENUM_VALUE(_cmd, _ep, _field, _value) \
+({ \
+	enum ast_geoloc_ ## _field v; \
+	if (!_ep) { \
+		ast_log(LOG_ERROR, "%s: Field %s requires a valid index\n", _cmd, #_field); \
+		return -1; \
+	} \
+	v = geoloc_ ## _field ## _str_to_enum(_value); \
+	if (v == AST_GEOLOC_INVALID_VALUE) { \
+		ast_log(LOG_ERROR, "%s: %s '%s' is invalid\n", _cmd, #_field, value); \
+		return -1; \
+	} \
+	_ep->_field = v; \
+})
+
+#define TEST_VARLIST(_cmd, _ep, _field, _value) \
+({ \
+	struct ast_variable *_list; \
+	if (!_ep) { \
+		ast_log(LOG_ERROR, "%s: Field %s requires a valid index\n", _cmd, #_field); \
+		return -1; \
+	} \
+	_list = ast_variable_list_from_quoted_string(_value, ",", "=", "\"" ); \
+	if (!_list) { \
+		ast_log(LOG_ERROR, "%s: %s '%s' is malformed or contains invalid values", _cmd, #_field, _value); \
+		return -1; \
+	} \
+	ast_variables_destroy(_ep->_field); \
+	_ep->_field = _list; \
+})
+
+static int geoloc_profile_write(struct ast_channel *chan, const char *cmd, char *data,
+	 const char *value)
+{
+	char *parsed_data = ast_strdupa(data);
+	struct ast_datastore *ds;
+	RAII_VAR(struct ast_geoloc_eprofile *, eprofile, NULL, ao2_cleanup);
+	int profile_count = 0;
+	int index = -1;
+
+	AST_DECLARE_APP_ARGS(args,
+		AST_APP_ARG(field);
+		AST_APP_ARG(index);
+	);
+
+	/* Check for zero arguments */
+	if (ast_strlen_zero(parsed_data)) {
+		ast_log(LOG_ERROR, "%s: Cannot call without arguments\n", cmd);
+		return -1;
+	}
+
+	AST_STANDARD_APP_ARGS(args, parsed_data);
+
+	if (ast_strlen_zero(args.field)) {
+		ast_log(LOG_ERROR, "%s: Cannot call without a field to set\n", cmd);
+		return -1;
+	}
+
+	if (!ast_strlen_zero(args.index)) {
+		if (sscanf(args.index, "%30d", &index) != 1) {
+			ast_log(LOG_ERROR, "%s: profile_index '%s' is invalid\n", cmd, args.index);
+			return -1;
+		}
+	}
+
+	ds = ast_geoloc_datastore_find(chan);
+	if (!ds) {
+		ast_log(LOG_WARNING, "%s: There are no geoloc profiles on this channel\n", cmd);
+		return -1;
+	}
+
+	profile_count = ast_geoloc_datastore_size(ds);
+
+	if (index >= 0 && index < profile_count) {
+		eprofile = ast_geoloc_datastore_get_eprofile(ds, index);
+		if (!eprofile) {
+			ast_log(LOG_ERROR, "%s: Internal Error.  Profile at index %d couldn't be retrieved.\n", cmd, index);
+			return -1;
+		}
+	} else if (index >= profile_count) {
+		ast_log(LOG_ERROR, "%s: index %d is out of range 0 -> %d\n", cmd, index, profile_count);
+		return -1;
+	} else {
+		if (ast_strings_equal(args.field, "inheritable")) {
+			ast_geoloc_datastore_set_inheritance(ds, ast_true(value));
+ 		} else {
+			ast_log(LOG_ERROR, "%s: Field '%s' is not valid or requires a profile index\n", cmd, args.field);
+			return -1;
+		}
+
+		return 0;
+	}
+
+	if (ast_strings_equal(args.field, "location_reference")) {
+		struct ast_geoloc_location *loc = ast_geoloc_get_location(value);
+		ao2_cleanup(loc);
+		if (!loc) {
+			ast_log(LOG_ERROR, "%s: Location reference '%s' doesn't exist\n", cmd, value);
+			return -1;
+		}
+		ast_string_field_set(eprofile, location_reference, value);
+	} else if (ast_strings_equal(args.field, "method")) {
+		ast_string_field_set(eprofile, method, value);
+
+	} else if (ast_strings_equal(args.field, "geolocation_routing")) {
+		eprofile->geolocation_routing = ast_true(value);
+
+	} else if (ast_strings_equal(args.field, "profile_action")) {
+		TEST_ENUM_VALUE(cmd, eprofile, action, value);
+
+	} else if (ast_strings_equal(args.field, "format")) {
+		TEST_ENUM_VALUE(cmd, eprofile, format, value);
+
+	} else if (ast_strings_equal(args.field, "pidf_element")) {
+		TEST_ENUM_VALUE(cmd, eprofile, pidf_element, value);
+
+	} else if (ast_strings_equal(args.field, "location_info")) {
+		TEST_VARLIST(cmd, eprofile, location_info, value);
+	} else if (ast_strings_equal(args.field, "location_source")) {
+		ast_string_field_set(eprofile, location_source, value);
+	} else if (ast_strings_equal(args.field, "location_info_refinement")) {
+		TEST_VARLIST(cmd, eprofile, location_refinement, value);
+	} else if (ast_strings_equal(args.field, "location_variables")) {
+		TEST_VARLIST(cmd, eprofile, location_variables, value);
+	} else if (ast_strings_equal(args.field, "effective_location")) {
+		TEST_VARLIST(cmd, eprofile, effective_location, value);
+	} else if (ast_strings_equal(args.field, "usage_rules")) {
+		TEST_VARLIST(cmd, eprofile, usage_rules, value);
+	} else {
+		ast_log(LOG_ERROR, "%s: Field '%s' is not valid\n", cmd, args.field);
+		return -1;
+	}
+
+	ast_geoloc_eprofile_refresh_location(eprofile);
+	return 0;
+}
+
+static struct ast_custom_function geoloc_function = {
+	.name = "GEOLOC_PROFILE",
+	.read2 = geoloc_profile_read,
+	.write = geoloc_profile_write,
+};
+
+#define profile_create "GeolocProfileCreate"
+
+static int geoloc_eprofile_create(struct ast_channel *chan, const char *data)
+{
+	char *parsed_data = ast_strdupa(data);
+	struct ast_datastore *ds;
+	struct ast_geoloc_eprofile * eprofile;
+	int profile_count = 0;
+	int index = -1;
+	int rc = 0;
+	struct ast_str *new_size;
+
+	AST_DECLARE_APP_ARGS(args,
+		AST_APP_ARG(id);
+		AST_APP_ARG(index);
+	);
+
+	/* Check for zero arguments */
+	if (ast_strlen_zero(parsed_data)) {
+		ast_log(LOG_ERROR, "%s: Cannot call without arguments\n", profile_create);
+		return -1;
+	}
+
+	AST_STANDARD_APP_ARGS(args, parsed_data);
+
+	if (ast_strlen_zero(args.id)) {
+		ast_log(LOG_ERROR, "%s: Cannot call without an id field\n", profile_create);
+		return -1;
+	}
+
+	if (!ast_strlen_zero(args.index)) {
+		if (sscanf(args.index, "%30d", &index) != 1) {
+			ast_log(LOG_ERROR, "%s: profile_index '%s' is invalid\n", profile_create, args.index);
+			return -1;
+		}
+	} else {
+		index = -1;
+	}
+
+	ds = ast_geoloc_datastore_find(chan);
+	if (!ds) {
+		ast_log(LOG_WARNING, "%s: There are no geoloc profiles on this channel\n", profile_create);
+		return -1;
+	}
+
+	profile_count = ast_geoloc_datastore_size(ds);
+	if (index < -1 || index >= profile_count) {
+		ast_log(LOG_ERROR, "%s: Invalid insert_before index '%d'.  It must be 0 to insert at the beginning of the list or -1 to append to the end of the list\n", profile_create, index);
+		return -1;
+	}
+
+	eprofile = ast_geoloc_eprofile_alloc(args.id);
+	if (!eprofile) {
+		ast_log(LOG_ERROR, "%s: Could not allocate eprofile '%s'\n", profile_create, args.id);
+		return -1;
+	}
+
+	ds = ast_geoloc_datastore_find(chan);
+	if (!ds) {
+		ds = ast_geoloc_datastore_create_from_eprofile(eprofile);
+		if (!ds) {
+			ao2_ref(eprofile, -1);
+			ast_log(LOG_ERROR, "%s: Could not create datastore for eprofile '%s'\n", profile_create, args.id);
+			return -1;
+		}
+		rc = 1;
+		ast_channel_datastore_add(chan, ds);
+	} else if (index < 0) {
+		rc = ast_geoloc_datastore_add_eprofile(ds, eprofile);
+		if (rc <= 0) {
+			ao2_ref(eprofile, -1);
+			ast_log(LOG_ERROR, "%s: Could not add eprofile '%s' to datastore\n", profile_create, args.id);
+			return -1;
+		}
+	} else {
+		rc = ast_geoloc_datastore_insert_eprofile(ds, eprofile, index);
+		if (rc <= 0) {
+			ao2_ref(eprofile, -1);
+			ast_log(LOG_ERROR, "%s: Could not insert eprofile '%s' to datastore\n", profile_create, args.id);
+			return -1;
+		}
+	}
+
+	new_size = ast_str_alloca(16);
+	ast_str_append(&new_size, 0, "%d", rc);
+	pbx_builtin_setvar_helper(chan, "GEOLOC_PROFILE_COUNT", ast_str_buffer(new_size));
+
+	return 0;
+}
+
+#define profile_delete "GeolocProfileDelete"
+
+static int geoloc_eprofile_delete(struct ast_channel *chan, const char *data)
+{
+	char *parsed_data = ast_strdupa(data);
+	struct ast_datastore *ds;
+	int profile_count = 0;
+	int index = -1;
+	struct ast_str *new_size;
+
+	AST_DECLARE_APP_ARGS(args,
+		AST_APP_ARG(index);
+	);
+
+	/* Check for zero arguments */
+	if (ast_strlen_zero(parsed_data)) {
+		ast_log(LOG_ERROR, "%s: Cannot call without arguments\n", profile_delete);
+		return -1;
+	}
+
+	AST_STANDARD_APP_ARGS(args, parsed_data);
+
+	if (!ast_strlen_zero(args.index)) {
+		if (sscanf(args.index, "%30d", &index) != 1) {
+			ast_log(LOG_ERROR, "%s: profile_index '%s' is invalid\n", profile_delete, args.index);
+			return -1;
+		}
+	} else {
+		ast_log(LOG_ERROR, "%s: A profile_index is required\n", profile_delete);
+		return -1;
+	}
+
+	ds = ast_geoloc_datastore_find(chan);
+	if (!ds) {
+		ast_log(LOG_WARNING, "%s: There are no geoloc profiles on this channel\n", profile_delete);
+		return -1;
+	}
+
+	profile_count = ast_geoloc_datastore_size(ds);
+	if (index < -1 || index >= profile_count) {
+		ast_log(LOG_ERROR, "%s: Invalid profile_index '%d'.  It must be between 0 and %d\n",
+			profile_create, index, profile_count - 1);
+		return -1;
+	}
+
+	ast_geoloc_datastore_delete_eprofile(ds, index);
+	profile_count = ast_geoloc_datastore_size(ds);
+
+	new_size = ast_str_alloca(16);
+	ast_str_append(&new_size, 0, "%d", profile_count);
+	pbx_builtin_setvar_helper(chan, "GEOLOC_PROFILE_COUNT", ast_str_buffer(new_size));
+
+	return 0;
+}
+
+int geoloc_dialplan_unload(void)
+{
+	ast_unregister_application(profile_delete);
+	ast_unregister_application(profile_create);
+	ast_custom_function_unregister(&geoloc_function);
+
+	return AST_MODULE_LOAD_SUCCESS;
+}
+
+int geoloc_dialplan_load(void)
+{
+	int res = 0;
+
+	res = ast_custom_function_register(&geoloc_function);
+	if (res == 0) {
+		res = ast_register_application_xml(profile_create, geoloc_eprofile_create);
+	}
+	if (res == 0) {
+		res = ast_register_application_xml(profile_delete, geoloc_eprofile_delete);
+	}
+
+	return res == 0 ? AST_MODULE_LOAD_SUCCESS : AST_MODULE_LOAD_DECLINE;
+}
+
+int geoloc_dialplan_reload(void)
+{
+	return AST_MODULE_LOAD_SUCCESS;
+}
+
diff --git a/res/res_geolocation/geoloc_doc.xml b/res/res_geolocation/geoloc_doc.xml
new file mode 100644
index 0000000..5c8bf79
--- /dev/null
+++ b/res/res_geolocation/geoloc_doc.xml
@@ -0,0 +1,235 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE docs SYSTEM "appdocsxml.dtd">
+<docs>
+	<configInfo name="res_geolocation" language="en_US">
+		<synopsis>Core Geolocation Support</synopsis>
+		<configFile name="geolocation.conf">
+			<configObject name="location">
+				<synopsis>Location</synopsis>
+				<description>
+					<para>cffdffff</para>
+				</description>
+				<configOption name="type">
+					<synopsis>Must be of type 'location'.</synopsis>
+				</configOption>
+				<configOption name="format" default="">
+					<synopsis>Location specification type</synopsis>
+					<description>
+						<enumlist>
+							<enum name="civicAddress">
+								<para>
+									The <literal>location_info</literal>
+									parameter must contain a comma separated list of IANA codes
+									or synonyms describing the civicAddress of this location.
+									The IANA codes and synonyms can be obtained by executing
+									the CLI command <literal>geoloc show civicAddr_mapping</literal>.
+								</para>
+							</enum>
+							<enum name="GML">
+								<para>
+									The
+									<literal>location_info</literal> parameter must contain a comma
+									separated list valid GML elements describing this location.
+								</para>
+							</enum>
+							<enum name="URI">
+								<para>
+									The
+									<literal>location_info</literal> parameter must contain a single
+									URI parameter which contains an external URI describing this location.
+								</para>
+							</enum>
+						</enumlist>
+					</description>
+				</configOption>
+				<configOption name="location_info" default="">
+					<synopsis>Location information</synopsis>
+					<description>
+						<para>The contents of this parameter are specific to the
+							location <literal>format</literal>.</para>
+						<enumlist>
+						<enum name="civicAddress">
+						<para>
+						location_info = country=US,A1="New York",city_district=Manhattan,
+						A3="New York", house_number=1633, street=46th, street_suffix = Street,
+						postal_code=10222,floor=20,room=20A2
+						</para>
+						</enum>
+						<enum name="GML">
+						<para>
+						location_info = Shape=Sphere, pos3d="39.12345 -105.98766 1920", radius=200
+						</para>
+						</enum>
+						<enum name="URI">
+						<para>
+						location_info = URI=https:/something.com?exten=${EXTEN}
+						</para>
+						</enum>
+						</enumlist>
+					</description>
+				</configOption>
+				<configOption name="location_source" default="">
+					<synopsis>Fully qualified host name</synopsis>
+					<description>
+						<para>This parameter isn't required but if provided, RFC8787 says it MUST be a fully
+						qualified host name.  IP addresses are specifically NOT allowed.  The value will be placed
+						in a <literal>loc-src</literal> parameter appended to the URI in the <literal>
+						Geolocation</literal> header.</para>
+					</description>
+				</configOption>
+				<configOption name="method" default="">
+					<synopsis>Location determination method</synopsis>
+					<description>
+						<para>This is a rarely used field in the specification that would
+						indicate the method used to determine the location.  Its usage and values should be
+						pre-negotiated with any recipients.</para>
+						<enumlist>
+						<enum name="GPS"/>
+						<enum name="A-GPS"/>
+						<enum name="Manual"/>
+						<enum name="DHCP"/>
+						<enum name="Triangulation"/>
+						<enum name="Cell"/>
+						<enum name="802.11"/>
+						</enumlist>
+					</description>
+				</configOption>
+			</configObject>
+			<configObject name="profile">
+				<synopsis>Profile</synopsis>
+				<description>
+					<para>cffdffff</para>
+				</description>
+				<configOption name="type">
+					<synopsis>Must be of type 'profile'.</synopsis>
+				</configOption>
+				<configOption name="pidf_element" default="device">
+					<synopsis>PIDF-LO element to place this profile in</synopsis>
+					<description>
+						<enumlist>
+							<enum name="tuple" />
+							<enum name="device" />
+							<enum name="person" />
+						</enumlist>
+						<para>
+							Based on RFC5491 (see below) the recommended and default element
+							is <literal>device</literal>.
+						</para>
+					</description>
+					<see-also>
+						<ref type="link">https://www.rfc-editor.org/rfc/rfc5491.html#section-3.4</ref>
+					</see-also>
+				</configOption>
+				<configOption name="location_reference" default="">
+					<synopsis>Reference to a location object</synopsis>
+				</configOption>
+				<configOption name="location_info_refinement" default="">
+					<synopsis>Reference to a location object</synopsis>
+				</configOption>
+				<configOption name="location_variables" default="">
+					<synopsis>Reference to a location object</synopsis>
+				</configOption>
+				<configOption name="usage_rules" default="yes">
+					<synopsis>location specification type</synopsis>
+					<description>
+						<para>xxxx</para>
+					</description>
+				</configOption>
+				<configOption name="notes" default="">
+					<synopsis>Notes to be added to the outgoing PIDF-LO document</synopsis>
+					<description>
+						<para>The specification of this parameter will cause a
+						<literal><note-well></literal> element to be added to the
+						outgoing PIDF-LO document.  Its usage should be pre-negotiated with
+						any recipients.</para>
+					</description>
+				</configOption>
+				<configOption name="profile_action" default="discard_incoming">
+					<synopsis>Determine which profile on a channel should be used</synopsis>
+					<description>
+						<enumlist>
+							<enum name="prefer_incoming">
+							<para>Use the incoming profile if it exists and has location information, otherwise use the
+							configured profile if it exists and has location information. If neither profile has location
+							information, nothing is sent.
+							</para></enum>
+							<enum name="prefer_config">
+							<para>Use the configured profile if it exists and has location information, otherwise use the
+							incoming profile if it exists and has location information. If neither profile has location
+							information, nothing is sent.
+							</para></enum>
+							<enum name="discard_incoming"
+							><para>Discard any incoming profile and use the configured profile if it exists and
+							it has location information.  If the configured profile doesn't exist or has no
+							location information, nothing is sent.
+							</para></enum>
+							<enum name="discard_config">
+							<para>Discard any configured profile and use the incoming profile if it exists and
+							it has location information.  If the incoming profile doesn't exist or has no
+							location information, nothing is sent.
+							</para></enum>
+						</enumlist>
+					</description>
+				</configOption>
+			</configObject>
+		</configFile>
+	</configInfo>
+	<function name="GEOLOC_PROFILE" language="en_US">
+		<synopsis>
+			Get or Set a field in a geolocation profile
+		</synopsis>
+		<syntax>
+			<parameter name="field" required="true">
+				<para>The profile field to operate on.</para>
+			</parameter>
+			<parameter name="profile_index" required="false">
+				<para>The index of the profile to operate on.  Not required for the special fields.</para>
+			</parameter>
+		</syntax>
+	</function>
+	<application name="GeolocProfileCreate" language="en_US">
+		<synopsis>
+			Create a new, empty Geolocation Profile on a channel
+		</synopsis>
+		<syntax>
+			<parameter name="id" required="true"><para>
+				The id of the new profile.
+			</para></parameter>
+			<parameter name="profile_index" required="false"><para>
+			 The position at which to insert the new eprofile.
+			 Existing profiles will be moved forward to make room.
+			 Leave empty to append to the end of the list.
+			</para></parameter>
+		</syntax>
+		<description>
+			<para>This application adds a new, empty Geolocation Profile to a channel.</para>
+			<para>The following variable is set:</para>
+			<variablelist>
+				<variable name="GEOLOC_PROFILE_COUNT">
+					<para>The number of profiles on the channel after the new one is created</para>
+				</variable>
+			</variablelist>
+		</description>
+	</application>
+	<application name="GeolocProfileDelete" language="en_US">
+		<synopsis>
+			Delete a Geolocation Profile from a channel
+		</synopsis>
+		<syntax>
+			<parameter name="profile_index" required="true"><para>
+			 The position of the profile to be deleted
+			 Existing profiles will be moved back.
+			</para></parameter>
+		</syntax>
+		<description>
+			<para>This application deletes a Geolocation Profile from a channel.</para>
+			<para>The following variable is set:</para>
+			<variablelist>
+				<variable name="GEOLOC_PROFILE_COUNT">
+					<para>The number of profiles left on the channel after the delete.</para>
+				</variable>
+			</variablelist>
+		</description>
+	</application>
+</docs>
+
diff --git a/res/res_geolocation/geoloc_eprofile.c b/res/res_geolocation/geoloc_eprofile.c
new file mode 100644
index 0000000..64244f4
--- /dev/null
+++ b/res/res_geolocation/geoloc_eprofile.c
@@ -0,0 +1,1002 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2022, Sangoma Technologies Corporation
+ *
+ * George Joseph <gjoseph at sangoma.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+#include "asterisk.h"
+#include "asterisk/pbx.h"
+#include "asterisk/strings.h"
+#include "asterisk/xml.h"
+#include "geoloc_private.h"
+
+extern const uint8_t _binary_res_geolocation_pidf_to_eprofile_xslt_start[];
+extern const uint8_t _binary_res_geolocation_pidf_to_eprofile_xslt_end[];
+static size_t pidf_to_eprofile_xslt_size;
+
+extern const uint8_t _binary_res_geolocation_pidf_lo_test_xml_start[];
+extern const uint8_t _binary_res_geolocation_pidf_lo_test_xml_end[];
+static size_t pidf_lo_test_xml_size;
+
+extern const uint8_t _binary_res_geolocation_eprofile_to_pidf_xslt_start[];
+extern const uint8_t _binary_res_geolocation_eprofile_to_pidf_xslt_end[];
+static size_t eprofile_to_pidf_xslt_size;
+
+static struct ast_xslt_doc *eprofile_to_pidf_xslt;
+static struct ast_xslt_doc *pidf_to_eprofile_xslt;
+
+static struct ast_sorcery *geoloc_sorcery;
+
+#define DUP_VARS(_dest, _source) \
+({ \
+	int _rc = 0; \
+	if (_source) { \
+		struct ast_variable *_vars = ast_variables_dup(_source); \
+		if (!_vars) { \
+			_rc = -1; \
+		} else { \
+			_dest = _vars; \
+		} \
+	} \
+	(_rc); \
+})
+
+static void geoloc_eprofile_destructor(void *obj)
+{
+	struct ast_geoloc_eprofile *eprofile = obj;
+
+	ast_string_field_free_memory(eprofile);
+	ast_variables_destroy(eprofile->location_info);
+	ast_variables_destroy(eprofile->location_refinement);
+	ast_variables_destroy(eprofile->location_variables);
+	ast_variables_destroy(eprofile->effective_location);
+	ast_variables_destroy(eprofile->usage_rules);
+}
+
+struct ast_geoloc_eprofile *ast_geoloc_eprofile_alloc(const char *name)
+{
+	struct ast_geoloc_eprofile *eprofile = ao2_alloc_options(sizeof(*eprofile),
+		geoloc_eprofile_destructor, AO2_ALLOC_OPT_LOCK_NOLOCK);
+
+	ast_string_field_init(eprofile, 256);
+	ast_string_field_set(eprofile, id, name); /* SAFE string fields handle NULL */
+
+	return eprofile;
+}
+
+int ast_geoloc_eprofile_refresh_location(struct ast_geoloc_eprofile *eprofile)
+{
+	struct ast_geoloc_location *loc = NULL;
+	struct ast_variable *temp_locinfo = NULL;
+	struct ast_variable *temp_effloc = NULL;
+	struct ast_variable *var;
+	int rc = 0;
+
+	if (!eprofile) {
+		return -1;
+	}
+
+	if (!ast_strlen_zero(eprofile->location_reference)) {
+		loc = ast_sorcery_retrieve_by_id(geoloc_sorcery, "location", eprofile->location_reference);
+		if (!loc) {
+			ast_log(LOG_ERROR, "Profile '%s' referenced location '%s' does not exist!", eprofile->id,
+				eprofile->location_reference);
+			return -1;
+		}
+
+		eprofile->format = loc->format;
+		rc = DUP_VARS(temp_locinfo, loc->location_info);
+		ast_string_field_set(eprofile, method, loc->method);
+		ao2_ref(loc, -1);
+		if (rc != 0) {
+			return -1;
+		}
+	} else {
+		temp_locinfo = eprofile->location_info;
+	}
+
+	rc = DUP_VARS(temp_effloc, temp_locinfo);
+	if (rc != 0) {
+		ast_variables_destroy(temp_locinfo);
+		return -1;
+	}
+
+	if (eprofile->location_refinement) {
+		for (var = eprofile->location_refinement; var; var = var->next) {
+			struct ast_variable *newvar = ast_variable_new(var->name, var->value, "");
+			if (!newvar) {
+				ast_variables_destroy(temp_locinfo);
+				ast_variables_destroy(temp_effloc);
+				return -1;
+			}
+			if (ast_variable_list_replace(&temp_effloc, newvar)) {
+				ast_variable_list_append(&temp_effloc, newvar);
+			}
+		}
+	}
+
+	ast_variables_destroy(eprofile->location_info);
+	eprofile->location_info = temp_locinfo;
+	ast_variables_destroy(eprofile->effective_location);
+	eprofile->effective_location = temp_effloc;
+
+	return 0;
+}
+
+struct ast_geoloc_eprofile *ast_geoloc_eprofile_create_from_profile(struct ast_geoloc_profile *profile)
+{
+	struct ast_geoloc_eprofile *eprofile;
+	const char *profile_id;
+	int rc = 0;
+
+	if (!profile) {
+		return NULL;
+	}
+
+	profile_id = ast_sorcery_object_get_id(profile);
+
+	eprofile = ast_geoloc_eprofile_alloc(profile_id);
+	if (!eprofile) {
+		return NULL;
+	}
+
+	ao2_lock(profile);
+	eprofile->geolocation_routing = profile->geolocation_routing;
+	eprofile->pidf_element = profile->pidf_element;
+
+	rc = ast_string_field_set(eprofile, location_reference, profile->location_reference);
+	if (rc == 0) {
+		ast_string_field_set(eprofile, notes, profile->notes);
+	}
+	if (rc == 0) {
+		rc = DUP_VARS(eprofile->location_refinement, profile->location_refinement);
+	}
+	if (rc == 0) {
+		rc = DUP_VARS(eprofile->location_variables, profile->location_variables);
+	}
+	if (rc == 0) {
+		rc = DUP_VARS(eprofile->usage_rules, profile->usage_rules);
+	}
+	if (rc != 0) {
+		ao2_unlock(profile);
+		ao2_ref(eprofile, -1);
+		return NULL;
+	}
+
+	eprofile->action = profile->action;
+	ao2_unlock(profile);
+
+	if (ast_geoloc_eprofile_refresh_location(eprofile) != 0) {
+		ao2_ref(eprofile, -1);
+		return NULL;
+	}
+
+	return eprofile;
+}
+
+static int set_loc_src(struct ast_geoloc_eprofile *eprofile, const char *uri, const char *ref_str)
+{
+	char *local_uri = ast_strdupa(uri);
+	char *loc_src = NULL;
+
+	loc_src = strchr(local_uri, ';');
+	if (loc_src) {
+		loc_src = '\0';
+		loc_src++;
+	}
+
+	if (!ast_strlen_zero(loc_src)) {
+		if (ast_begins_with(loc_src, "loc-src=")) {
+			struct ast_sockaddr loc_source_addr;
+			int rc = 0;
+			loc_src += 8;
+			rc = ast_sockaddr_parse(&loc_source_addr, loc_src, PARSE_PORT_FORBID);
+			if (rc == 1) {
+				ast_log(LOG_WARNING, "%s: URI '%s' has an invalid 'loc-src' parameter."
+					" RFC8787 states that IP addresses MUST be dropped.\n",
+					ref_str, uri);
+				return -1;
+			} else {
+				ast_string_field_set(eprofile, location_source, loc_src);
+			}
+		}
+	}
+	return 0;
+}
+
+struct ast_geoloc_eprofile *ast_geoloc_eprofile_create_from_uri(const char *uri,
+	const char *ref_str)
+{
+	struct ast_geoloc_eprofile *eprofile = NULL;
+	char *ra = NULL;
+	char *local_uri;
+
+	if (ast_strlen_zero(uri)) {
+		return NULL;
+	}
+	local_uri = ast_strdupa(uri);
+
+	if (local_uri[0] == '<') {
+		local_uri++;
+	}
+	ra = strchr(local_uri, '>');
+	if (ra) {
+		*ra = '\0';
+	}
+
+	ast_strip(local_uri);
+
+	eprofile = ast_geoloc_eprofile_alloc(local_uri);
+	if (!eprofile) {
+		return NULL;
+	}
+
+	set_loc_src(eprofile, uri, ref_str);
+
+	eprofile->format = AST_GEOLOC_FORMAT_URI;
+	eprofile->location_info = ast_variable_new("URI", local_uri, "");
+
+	return eprofile;
+}
+
+static struct ast_variable *geoloc_eprofile_resolve_varlist(struct ast_variable *source,
+	struct ast_variable *variables, struct ast_channel *chan)
+{
+	struct ast_variable *dest = NULL;
+	struct ast_variable *var = NULL;
+	struct varshead *vh = NULL;
+	struct ast_str *buf = ast_str_alloca(256);
+
+	if (!source || !chan) {
+		return NULL;
+	}
+
+	/*
+	 * ast_str_substitute_variables does only minimal recursive resolution so we need to
+	 * pre-resolve each variable in the "variables" list, then use that result to
+	 * do the final pass on the "source" variable list.
+	 */
+	if (variables) {
+		var = variables;
+		vh = ast_var_list_create();
+		if (!vh) {
+			return NULL;
+		}
+		for ( ; var; var = var->next) {
+			ast_str_substitute_variables_full2(&buf, 0, chan, vh, var->value, NULL, 1);
+			AST_VAR_LIST_INSERT_TAIL(vh, ast_var_assign(var->name, ast_str_buffer(buf)));
+			ast_str_reset(buf);
+		}
+	}
+
+	var = source;
+	for ( ; var; var = var->next) {
+		struct ast_variable *newvar = NULL;
+		ast_str_substitute_variables_full2(&buf, 0, chan, vh, var->value, NULL, 1);
+		newvar = ast_variable_new(var->name, ast_str_buffer(buf), "");
+		if (!newvar) {
+			ast_variables_destroy(dest);
+			ast_var_list_destroy(vh);
+			return NULL;
+		}
+		ast_variable_list_append(&dest, newvar);
+		ast_str_reset(buf);
+	}
+	ast_var_list_destroy(vh);
+
+	return dest;
+}
+
+
+const char *ast_geoloc_eprofile_to_uri(struct ast_geoloc_eprofile *eprofile,
+	struct ast_channel *chan, struct ast_str **buf, const char *ref_string)
+{
+	const char *uri = NULL;
+	struct ast_variable *resolved = NULL;
+	char *result;
+	int we_created_buf = 0;
+
+	if (!eprofile || !buf) {
+		return NULL;
+	}
+
+	if (eprofile->format != AST_GEOLOC_FORMAT_URI) {
+		ast_log(LOG_ERROR, "%s: '%s' is not a URI profile.  It's '%s'\n",
+			ref_string, eprofile->id, geoloc_format_to_name(eprofile->format));
+		return NULL;
+	}
+
+	resolved = geoloc_eprofile_resolve_varlist(eprofile->effective_location,
+		eprofile->location_variables, chan);
+	if (!resolved) {
+		return NULL;
+	}
+
+	uri = ast_variable_find_in_list(resolved, "URI");
+	result = uri ? ast_strdupa(uri) : NULL;
+	ast_variables_destroy(resolved);
+
+	if (ast_strlen_zero(result)) {
+		ast_log(LOG_ERROR, "%s: '%s' is a URI profile but had no, or an empty, 'URI' entry in location_info\n",
+			ref_string, eprofile->id);
+		return NULL;
+	}
+
+	if (!*buf) {
+		*buf = ast_str_create(256);
+		if (!*buf) {
+			return NULL;
+		}
+		we_created_buf = 1;
+	}
+
+	if (ast_str_append(buf, 0, "%s", result) <= 0) {
+		if (we_created_buf) {
+			ast_free(*buf);
+			*buf = NULL;
+			return NULL;
+		}
+	}
+
+	return ast_str_buffer(*buf);
+}
+
+static struct ast_variable *var_list_from_node(struct ast_xml_node *node, const char *reference_string)
+{
+	struct ast_variable *list = NULL;
+	struct ast_xml_node *container;
+	struct ast_xml_node *child;
+	struct ast_variable *var;
+	RAII_VAR(struct ast_str *, buf, NULL, ast_free);
+	SCOPE_ENTER(3, "%s\n", reference_string);
+
+	container = ast_xml_node_get_children(node);
+	for (child = container; child; child = ast_xml_node_get_next(child)) {
+		const char *name = ast_xml_node_get_name(child);
+		const char *value = ast_xml_get_text(child);
+		const char *uom = ast_xml_get_attribute(child, "uom");
+
+		if (uom) {
+			/* '20 radians\0' */
+			char *newval = ast_malloc(strlen(value) + 1 + strlen(uom) + 1);
+			sprintf(newval, "%s %s", value, uom);
+			var = ast_variable_new(name, newval, "");
+			ast_free(newval);
+		} else {
+			var = ast_variable_new(name, value, "");
+		}
+
+		if (!var) {
+			ast_variables_destroy(list);
+			SCOPE_EXIT_RTN_VALUE(NULL, "%s: Allocation failure\n", reference_string);
+		}
+		ast_variable_list_append(&list, var);
+	}
+
+	ast_variable_list_join(list, ", ", "=", "\"", &buf);
+
+	SCOPE_EXIT_RTN_VALUE(list, "%s: Done. %s\n", reference_string, ast_str_buffer(buf));
+}
+
+static struct ast_variable *var_list_from_loc_info(struct ast_xml_node *locinfo,
+	enum ast_geoloc_format format, const char *reference_string)
+{
+	struct ast_variable *list = NULL;
+	struct ast_xml_node *container;
+	struct ast_variable *var;
+	RAII_VAR(struct ast_str *, buf, NULL, ast_free);
+	SCOPE_ENTER(3, "%s\n", reference_string);
+
+	container = ast_xml_node_get_children(locinfo);
+	if (format == AST_GEOLOC_FORMAT_CIVIC_ADDRESS) {
+		var = ast_variable_new("lang", ast_xml_get_attribute(container, "lang"), "");
+		if (!var) {
+			SCOPE_EXIT_RTN_VALUE(NULL, "%s: Allocation failure\n", reference_string);
+		}
+		ast_variable_list_append(&list, var);
+	} else {
+		var = ast_variable_new("shape", ast_xml_node_get_name(container), "");
+		if (!var) {
+			SCOPE_EXIT_RTN_VALUE(NULL, "%s: Allocation failure\n", reference_string);
+		}
+		ast_variable_list_append(&list, var);
+		var = ast_variable_new("crs", ast_xml_get_attribute(container, "srsName"), "");
+		if (!var) {
+			ast_variables_destroy(list);
+			SCOPE_EXIT_RTN_VALUE(NULL, "%s: Allocation failure\n", reference_string);
+		}
+		ast_variable_list_append(&list, var);
+	}
+
+	ast_variable_list_append(&list, var_list_from_node(container, reference_string));
+
+	ast_variable_list_join(list, ", ", "=", "\"", &buf);
+
+	SCOPE_EXIT_RTN_VALUE(list, "%s: Done. %s\n", reference_string, ast_str_buffer(buf));
+}
+
+static struct ast_geoloc_eprofile *geoloc_eprofile_create_from_xslt_result(
+	struct ast_xml_doc *result_doc,
+	const char *reference_string)
+{
+	struct ast_geoloc_eprofile *eprofile;
+	struct ast_xml_node *presence = NULL;
+	struct ast_xml_node *pidf_element = NULL;
+	struct ast_xml_node *location_info = NULL;
+	struct ast_xml_node *usage_rules = NULL;
+	struct ast_xml_node *method = NULL;
+	struct ast_xml_node *note_well = NULL;
+	char *doc_str;
+	int doc_len;
+	const char *id;
+	const char *format_str;
+	const char *pidf_element_str;
+	const char *method_str;
+	const char *note_well_str;
+	SCOPE_ENTER(3, "%s\n", reference_string);
+
+	ast_xml_doc_dump_memory(result_doc, &doc_str, &doc_len);
+	ast_trace(5, "xslt result doc:\n%s\n", doc_str);
+	ast_xml_free_text(doc_str);
+
+	presence = ast_xml_get_root(result_doc);
+	pidf_element = ast_xml_node_get_children(presence);
+	location_info = ast_xml_find_child_element(pidf_element, "location-info", NULL, NULL);
+	usage_rules = ast_xml_find_child_element(pidf_element, "usage-rules", NULL, NULL);
+	method = ast_xml_find_child_element(pidf_element, "method", NULL, NULL);
+	note_well = ast_xml_find_child_element(pidf_element, "note-well", NULL, NULL);
+
+	id = S_OR(ast_xml_get_attribute(pidf_element, "id"), ast_xml_get_attribute(presence, "entity"));
+	eprofile = ast_geoloc_eprofile_alloc(id);
+	if (!eprofile) {
+		SCOPE_EXIT_RTN_VALUE(NULL, "%s: Allocation failure\n", reference_string);
+	}
+
+	format_str = ast_xml_get_attribute(location_info, "format");
+	if (strcasecmp(format_str, "gml") == 0) {
+		eprofile->format = AST_GEOLOC_FORMAT_GML;
+	} else if (strcasecmp(format_str, "civicAddress") == 0) {
+		eprofile->format = AST_GEOLOC_FORMAT_CIVIC_ADDRESS;
+	} else {
+		ao2_ref(eprofile, -1);
+		SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unknown format '%s'\n", reference_string, format_str);
+	}
+
+	pidf_element_str = ast_xml_node_get_name(pidf_element);
+	eprofile->pidf_element = geoloc_pidf_element_str_to_enum(pidf_element_str);
+
+	eprofile->location_info = var_list_from_loc_info(location_info, eprofile->format, reference_string);
+	if (!eprofile->location_info) {
+		ao2_ref(eprofile, -1);
+		SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR,
+			"%s: Unable to create location variables\n", reference_string);
+	}
+
+	eprofile->usage_rules = var_list_from_node(usage_rules, reference_string);
+
+	method_str = ast_xml_get_text(method);
+	ast_string_field_set(eprofile, method, method_str);
+
+	note_well_str = ast_xml_get_text(note_well);
+	ast_string_field_set(eprofile, notes, note_well_str);
+
+	SCOPE_EXIT_RTN_VALUE(eprofile, "%s: Done.\n", reference_string);
+}
+
+struct ast_geoloc_eprofile *ast_geoloc_eprofile_create_from_pidf(
+	struct ast_xml_doc *pidf_xmldoc, const char *geoloc_uri, const char *ref_str)
+{
+	RAII_VAR(struct ast_xml_doc *, result_doc, NULL, ast_xml_close);
+	struct ast_geoloc_eprofile *eprofile;
+	/*
+	 * The namespace prefixes used here (dm, def, gp, etc) don't have to match
+	 * the ones used in the received PIDF-LO document but they MUST match the
+	 * ones in the embedded pidf_to_eprofile stylesheet.
+	 *
+	 * RFC5491 Rule 8 states that...
+	 * Where a PIDF document contains more than one <geopriv>
+     * element, the priority of interpretation is given to the first
+     * <device> element in the document containing a location.  If no
+     * <device> element containing a location is present in the document,
+     * then priority is given to the first <tuple> element containing a
+     * location.  Locations contained in <person> tuples SHOULD only be
+     * used as a last resort.
+     *
+     * Reminder: xpath arrays are 1-based not 0-based.
+	 */
+	const char *find_device[] = { "path", "/def:presence/dm:device[.//gp:location-info][1]", NULL};
+	const char *find_tuple[] = { "path", "/def:presence/def:tuple[.//gp:location-info][1]", NULL};
+	const char *find_person[] = { "path", "/def:presence/dm:person[.//gp:location-info][1]", NULL};
+	SCOPE_ENTER(3, "%s\n", ref_str);
+
+
+	result_doc = ast_xslt_apply(pidf_to_eprofile_xslt, pidf_xmldoc, find_device);
+	if (!result_doc || !ast_xml_node_get_children((struct ast_xml_node *)result_doc)) {
+		ast_xml_close(result_doc);
+		result_doc = ast_xslt_apply(pidf_to_eprofile_xslt, pidf_xmldoc, find_tuple);
+	}
+	if (!result_doc || !ast_xml_node_get_children((struct ast_xml_node *)result_doc)) {
+		ast_xml_close(result_doc);
+		result_doc = ast_xslt_apply(pidf_to_eprofile_xslt, pidf_xmldoc, find_person);
+	}
+	if (!result_doc || !ast_xml_node_get_children((struct ast_xml_node *)result_doc)) {
+		SCOPE_EXIT_RTN_VALUE(NULL, "%s: Not a PIDF-LO.  Skipping.\n", ref_str);
+	}
+
+	/*
+	 * The document returned from the stylesheet application looks like this...
+	 * <presence id="presence-entity">
+	 *     <tuple id="element-id">
+	 *         <location-info format="gml">shape="Ellipsoid", crs="3d", ...</location-info>
+	 *         <usage-rules>retransmission-allowed="no", retention-expiry="2010-11-14T20:00:00Z"</usage-rules>
+	 *         <method>Hybrid_A-GPS</method>
+	 *     </tuple>
+	 *  </presence>
+	 *
+	 * Regardless of whether the pidf-element was tuple, device or person and whether
+	 * the format is gml or civicAddress, the presence, pidf-element, location-info,
+	 * usage-rules and method elements should be there although usage-rules and method
+	 * may be empty.
+	 *
+	 * The contents of the location-info and usage-rules elements can be passed directly to
+	 * ast_variable_list_from_string().
+	 */
+
+	eprofile = geoloc_eprofile_create_from_xslt_result(result_doc, ref_str);
+
+	if (geoloc_uri) {
+		set_loc_src(eprofile, geoloc_uri, ref_str);
+	}
+
+	SCOPE_EXIT_RTN_VALUE(eprofile, "%s: Done.\n", ref_str);
+}
+
+/*!
+ * \internal
+ * \brief Create an common, intermediate XML document to pass to the outgoing XSLT process
+ *
+ * \param eprofile    The eprofile
+ * \param chan        The channel to resolve variables against
+ * \param ref_string  A reference string for error messages
+ * \return            An XML doc
+ *
+ * \note Given that the document is simple and static, it was easier to just
+ * create the elements in a string buffer and call ast_xml_read_memory()
+ * at the end instead of creating
+ *
+ */
+static struct ast_xml_node *geoloc_eprofile_to_intermediate(const char *element_name, struct ast_geoloc_eprofile *eprofile,
+	struct ast_channel *chan, const char *ref_string)
+{
+	struct ast_variable *resolved_location = NULL;
+	struct ast_variable *resolved_usage = NULL;
+	struct ast_variable *var = NULL;
+	RAII_VAR(struct ast_xml_node *, pidf_node, NULL, ast_xml_free_node);
+	struct ast_xml_node *rtn_pidf_node;
+	struct ast_xml_node *loc_node;
+	struct ast_xml_node *info_node;
+	struct ast_xml_node *rules_node;
+	struct ast_xml_node *method_node;
+	struct ast_xml_node *notes_node;
+	struct ast_xml_node *timestamp_node;
+	struct timeval tv = ast_tvnow();
+	struct tm tm = { 0, };
+	char timestr[32] = { 0, };
+	int rc = 0;
+
+	SCOPE_ENTER(3, "%s\n", ref_string);
+
+	if (!eprofile || !chan) {
+		SCOPE_EXIT_RTN_VALUE(NULL, "%s: Either or both eprofile or chan were NULL\n", ref_string);
+	}
+
+	pidf_node = ast_xml_new_node(element_name);
+	if (!pidf_node) {
+		SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create 'pidf' XML node\n",
+			ref_string);
+	}
+
+	loc_node = ast_xml_new_child(pidf_node, "location-info");
+	if (!loc_node) {
+		SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create 'location-info' XML node\n",
+			ref_string);
+	}
+	rc = ast_xml_set_attribute(loc_node, "format", geoloc_format_to_name(eprofile->format));
+	if (rc != 0) {
+		SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to set 'format' XML attribute\n", ref_string);
+	}
+
+	resolved_location = geoloc_eprofile_resolve_varlist(eprofile->effective_location,
+		eprofile->location_variables, chan);
+	if (eprofile->format == AST_GEOLOC_FORMAT_CIVIC_ADDRESS) {
+		info_node = geoloc_civicaddr_list_to_xml(resolved_location, ref_string);
+	} else {
+		info_node = geoloc_gml_list_to_xml(resolved_location, ref_string);
+	}
+	if (!info_node) {
+		SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create XML from '%s' list\n",
+			ref_string, geoloc_format_to_name(eprofile->format));
+	}
+	if (!ast_xml_add_child(loc_node, info_node)) {
+		ast_xml_free_node(info_node);
+		SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable add '%s' node to XML document\n",
+			ref_string, geoloc_format_to_name(eprofile->format));
+	}
+
+	rules_node = ast_xml_new_child(pidf_node, "usage-rules");
+	if (!rules_node) {
+		SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create 'usage-rules' XML node\n",
+			ref_string);
+	}
+	resolved_usage = geoloc_eprofile_resolve_varlist(eprofile->usage_rules,
+		eprofile->location_variables, chan);
+	for (var = resolved_usage; var; var = var->next) {
+		struct ast_xml_node *ur = ast_xml_new_child(rules_node, var->name);
+		ast_xml_set_text(ur, var->value);
+	}
+
+	if (!ast_strlen_zero(eprofile->method)) {
+		method_node = ast_xml_new_child(pidf_node, "method");
+		if (!method_node) {
+			SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create 'method' XML node\n",
+				ref_string);
+		}
+		ast_xml_set_text(method_node, eprofile->method);
+	};
+
+	if (!ast_strlen_zero(eprofile->notes)) {
+		notes_node = ast_xml_new_child(pidf_node, "note-well");
+		if (!notes_node) {
+			SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create 'note-well' XML node\n",
+				ref_string);
+		}
+		ast_xml_set_text(notes_node, eprofile->notes);
+	};
+
+	gmtime_r(&tv.tv_sec, &tm);
+	strftime(timestr, sizeof(timestr), "%FT%TZ", &tm);
+	timestamp_node = ast_xml_new_child(pidf_node, "timestamp");
+	if (!timestamp_node) {
+		SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create 'timestamp' XML node\n",
+			ref_string);
+	}
+	ast_xml_set_text(timestamp_node, timestr);
+
+	rtn_pidf_node = pidf_node;
+	pidf_node = NULL;
+	SCOPE_EXIT_RTN_VALUE(rtn_pidf_node, "%s: Done\n", ref_string);
+}
+
+#define CREATE_NODE_LIST(node) \
+	if (!node) { \
+		node = ast_xml_new_child(root_node, \
+			geoloc_pidf_element_to_name(eprofile->pidf_element)); \
+		if (!pidfs[eprofile->pidf_element]) { \
+			SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create pidf '%s' XML node\n", \
+				ref_string, geoloc_pidf_element_to_name(eprofile->pidf_element)); \
+		} \
+	}
+
+const char *ast_geoloc_eprofiles_to_pidf(struct ast_datastore *ds,
+	struct ast_channel *chan, struct ast_str **buf, const char * ref_string)
+{
+	RAII_VAR(struct ast_xml_doc *, intermediate, NULL, ast_xml_close);
+	RAII_VAR(struct ast_xml_doc *, pidf_doc, NULL, ast_xml_close);
+	struct ast_xml_node *root_node;
+	struct ast_xml_node *pidfs[AST_PIDF_ELEMENT_LAST] = {NULL, };
+	struct ast_geoloc_eprofile *eprofile;
+	int eprofile_count = 0;
+	int i;
+	RAII_VAR(char *, doc_str, NULL, ast_xml_free_text);
+	int doc_len;
+	int rc = 0;
+	SCOPE_ENTER(3, "%s\n", ref_string);
+
+	if (!ds || !chan || !buf || !*buf || ast_strlen_zero(ref_string)) {
+		SCOPE_EXIT_RTN_VALUE(NULL, "%s: Either or both datastore or chan were NULL\n",
+			ref_string);
+	}
+
+	intermediate = ast_xml_new();
+	if (!intermediate) {
+		SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create XML document\n", ref_string);
+	}
+	root_node = ast_xml_new_node("presence");
+	if (!root_node) {
+		SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create root XML node\n", ref_string);
+	}
+	ast_xml_set_root(intermediate, root_node);
+
+	eprofile_count = ast_geoloc_datastore_size(ds);
+	for (i = 0; i < eprofile_count; i++) {
+		struct ast_xml_node *temp_node = NULL;
+		struct ast_xml_node *curr_loc = NULL;
+		struct ast_xml_node *new_loc = NULL;
+		struct ast_xml_node *new_loc_child = NULL;
+		struct ast_xml_node *new_loc_child_dup = NULL;
+		eprofile = ast_geoloc_datastore_get_eprofile(ds, i);
+		if (eprofile->format == AST_GEOLOC_FORMAT_URI) {
+			continue;
+		}
+
+		if (ast_strlen_zero(ast_xml_get_attribute(root_node, "entity"))) {
+			rc = ast_xml_set_attribute(root_node, "entity", eprofile->id);
+			if (rc != 0) {
+				SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to set 'entity' XML attribute\n", ref_string);
+			}
+		}
+
+		temp_node = geoloc_eprofile_to_intermediate(geoloc_pidf_element_to_name(eprofile->pidf_element),
+			eprofile, chan, ref_string);
+
+		if (!pidfs[eprofile->pidf_element]) {
+			pidfs[eprofile->pidf_element] = temp_node;
+			ast_xml_add_child(root_node, temp_node);
+			continue;
+		}
+
+		curr_loc = ast_xml_find_child_element(pidfs[eprofile->pidf_element], "location-info", NULL, NULL);
+		new_loc = ast_xml_find_child_element(temp_node, "location-info", NULL, NULL);
+		new_loc_child = ast_xml_node_get_children(new_loc);
+		new_loc_child_dup = ast_xml_copy_node_list(new_loc_child);
+		ast_xml_add_child_list(curr_loc, new_loc_child_dup);
+
+		ast_xml_free_node(temp_node);
+	}
+
+	ast_xml_doc_dump_memory(intermediate, &doc_str, &doc_len);
+	if (doc_len == 0 || !doc_str) {
+		SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to dump intermediate doc to string\n",
+			ref_string);
+	}
+
+	ast_trace(5, "Intermediate doc:\n%s\n", doc_str);
+
+	pidf_doc = ast_xslt_apply(eprofile_to_pidf_xslt, intermediate, NULL);
+	if (!pidf_doc) {
+		SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create final PIDF-LO doc from intermediate doc:\n%s\n",
+			ref_string, doc_str);
+	}
+
+	ast_xml_doc_dump_memory(pidf_doc, &doc_str, &doc_len);
+	if (doc_len == 0 || !doc_str) {
+		SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to dump final PIDF-LO doc to string\n",
+			ref_string);
+	}
+
+	rc = ast_str_set(buf, 0, "%s", doc_str);
+	if (rc <= 0) {
+		SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to extend buffer (%d)\n",
+			ref_string, rc);
+	}
+
+	ast_trace(5, "Final doc:\n%s\n", ast_str_buffer(*buf));
+
+	SCOPE_EXIT_RTN_VALUE(ast_str_buffer(*buf), "%s: Done\n", ref_string);
+}
+
+#ifdef TEST_FRAMEWORK
+static void load_tests(void);
+static void unload_tests(void);
+#else
+static void load_tests(void) {}
+static void unload_tests(void) {}
+#endif
+
+
+int geoloc_eprofile_unload(void)
+{
+	unload_tests();
+	if (pidf_to_eprofile_xslt) {
+		ast_xslt_close(pidf_to_eprofile_xslt);
+	}
+
+	if (eprofile_to_pidf_xslt) {
+		ast_xslt_close(eprofile_to_pidf_xslt);
+	}
+
+	if (geoloc_sorcery) {
+		ast_sorcery_unref(geoloc_sorcery);
+	}
+
+	return AST_MODULE_LOAD_SUCCESS;
+}
+
+int geoloc_eprofile_load(void)
+{
+	pidf_to_eprofile_xslt_size =
+		(_binary_res_geolocation_pidf_to_eprofile_xslt_end - _binary_res_geolocation_pidf_to_eprofile_xslt_start);
+
+	pidf_lo_test_xml_size =
+		(_binary_res_geolocation_pidf_lo_test_xml_end - _binary_res_geolocation_pidf_lo_test_xml_start);
+
+	pidf_to_eprofile_xslt = ast_xslt_read_memory(
+		(char *)_binary_res_geolocation_pidf_to_eprofile_xslt_start, pidf_to_eprofile_xslt_size);
+	if (!pidf_to_eprofile_xslt) {
+		ast_log(LOG_ERROR, "Unable to read pidf_to_eprofile_xslt from memory\n");
+		return AST_MODULE_LOAD_DECLINE;
+	}
+
+	eprofile_to_pidf_xslt_size =
+		(_binary_res_geolocation_eprofile_to_pidf_xslt_end - _binary_res_geolocation_eprofile_to_pidf_xslt_start);
+
+	eprofile_to_pidf_xslt = ast_xslt_read_memory(
+		(char *)_binary_res_geolocation_eprofile_to_pidf_xslt_start, eprofile_to_pidf_xslt_size);
+	if (!eprofile_to_pidf_xslt) {
+		ast_log(LOG_ERROR, "Unable to read eprofile_to_pidf_xslt from memory\n");
+//		geoloc_eprofile_unload();
+		return AST_MODULE_LOAD_DECLINE;
+	}
+
+	geoloc_sorcery = geoloc_get_sorcery();
+
+	load_tests();
+
+	return AST_MODULE_LOAD_SUCCESS;
+}
+
+int geoloc_eprofile_reload(void)
+{
+	return AST_MODULE_LOAD_SUCCESS;
+}
+
+
+#ifdef TEST_FRAMEWORK
+#include "asterisk/test.h"
+
+AST_TEST_DEFINE(test_create_from_uri)
+{
+
+	RAII_VAR(struct ast_geoloc_eprofile *, eprofile,  NULL, ao2_cleanup);
+	const char *uri = NULL;
+	int rc = AST_TEST_PASS;
+
+	switch (cmd) {
+	case TEST_INIT:
+		info->name = "create_from_uri";
+		info->category = "/geoloc/";
+		info->summary = "Test create from uri";
+		info->description = info->summary;
+		return AST_TEST_NOT_RUN;
+	case TEST_EXECUTE:
+		break;
+	}
+
+	eprofile = ast_geoloc_eprofile_create_from_uri("http://some_uri&a=b", __func__);
+	ast_test_validate(test, eprofile != NULL);
+	ast_test_validate(test, eprofile->format == AST_GEOLOC_FORMAT_URI);
+	ast_test_validate(test, eprofile->location_info != NULL);
+	uri = ast_variable_find_in_list(eprofile->location_info, "URI");
+	ast_test_validate(test, uri != NULL);
+	ast_test_validate(test, strcmp(uri, "http://some_uri&a=b") == 0);
+
+	return rc;
+}
+
+static enum ast_test_result_state validate_eprofile(struct ast_test *test,
+	struct ast_xml_doc * pidf_xmldoc,
+	const char *path,
+	const char *id,
+	enum ast_geoloc_pidf_element pidf_element,
+	enum ast_geoloc_format format,
+	const char *method,
+	const char *location,
+	const char *usage
+	)
+{
+	RAII_VAR(struct ast_str *, str, NULL, ast_free);
+	RAII_VAR(struct ast_geoloc_eprofile *, eprofile,  NULL, ao2_cleanup);
+	RAII_VAR(struct ast_xml_doc *, result_doc, NULL, ast_xml_close);
+	const char *search[] = { "path", path, NULL };
+
+	if (!ast_strlen_zero(path)) {
+		result_doc = ast_xslt_apply(pidf_to_eprofile_xslt, pidf_xmldoc, (const char **)search);
+		ast_test_validate(test, (result_doc && ast_xml_node_get_children((struct ast_xml_node *)result_doc)));
+
+		eprofile = geoloc_eprofile_create_from_xslt_result(result_doc, "test_create_from_xslt");
+	} else {
+		eprofile = ast_geoloc_eprofile_create_from_pidf(pidf_xmldoc, NULL, "test_create_from_pidf");
+	}
+
+	ast_test_validate(test, eprofile != NULL);
+	ast_test_status_update(test, "ID: '%s'  pidf_element: '%s'  format: '%s'  method: '%s'\n", eprofile->id,
+		geoloc_pidf_element_to_name(eprofile->pidf_element),
+		geoloc_format_to_name(eprofile->format),
+		eprofile->method);
+
+	ast_test_validate(test, ast_strings_equal(eprofile->id, id));
+	ast_test_validate(test, eprofile->pidf_element == pidf_element);
+	ast_test_validate(test, eprofile->format == format);
+	ast_test_validate(test, ast_strings_equal(eprofile->method, method));
+
+	str = ast_variable_list_join(eprofile->location_info, ",", "=", NULL, NULL);
+	ast_test_validate(test, str != NULL);
+	ast_test_status_update(test, "location_vars expected: %s\n", location);
+	ast_test_status_update(test, "location_vars received: %s\n", ast_str_buffer(str));
+	ast_test_validate(test, ast_strings_equal(ast_str_buffer(str), location));
+	ast_free(str);
+
+	str = ast_variable_list_join(eprofile->usage_rules, ",", "=", "'", NULL);
+	ast_test_validate(test, str != NULL);
+	ast_test_status_update(test, "usage_rules expected: %s\n", usage);
+	ast_test_status_update(test, "usage_rules received: %s\n", ast_str_buffer(str));
+	ast_test_validate(test, ast_strings_equal(ast_str_buffer(str), usage));
+
+	return AST_TEST_PASS;
+}
+
+AST_TEST_DEFINE(test_create_from_pidf)
+{
+
+	RAII_VAR(struct ast_xml_doc *, pidf_xmldoc, NULL, ast_xml_close);
+	enum ast_test_result_state res = AST_TEST_PASS;
+
+	switch (cmd) {
+	case TEST_INIT:
+		info->name = "create_from_pidf";
+		info->category = "/geoloc/";
+		info->summary = "Test create from pidf scenarios";
+		info->description = info->summary;
+		return AST_TEST_NOT_RUN;
+	case TEST_EXECUTE:
+		break;
+	}
+
+	pidf_xmldoc = ast_xml_read_memory((char *)_binary_res_geolocation_pidf_lo_test_xml_start, pidf_lo_test_xml_size);
+	ast_test_validate(test, pidf_xmldoc != NULL);
+
+	res = validate_eprofile(test, pidf_xmldoc,
+		NULL,
+		"arcband-2d",
+		AST_PIDF_ELEMENT_DEVICE,
+		AST_GEOLOC_FORMAT_GML,
+		"TA-NMR",
+		"shape=ArcBand,crs=2d,pos=-43.5723 153.21760,innerRadius=3594,"
+				"outerRadius=4148,startAngle=20 radians,openingAngle=20 radians",
+		"retransmission-allowed='yes',ruleset-preference='https:/www/more.com',"
+			"retention-expires='2007-06-22T20:57:29Z'"
+		);
+	ast_test_validate(test, res == AST_TEST_PASS);
+
+
+	res = validate_eprofile(test, pidf_xmldoc,
+		"/def:presence/dm:device[.//ca:civicAddress][1]",
+		"pres:alice at asterisk.org",
+		AST_PIDF_ELEMENT_DEVICE,
+		AST_GEOLOC_FORMAT_CIVIC_ADDRESS,
+		"GPS",
+		"lang=en-AU,country=AU,A1=NSW,A3=Wollongong,A4=North Wollongong,"
+			"RD=Flinders,STS=Street,RDBR=Campbell Street,LMK=Gilligan's Island,"
+			"LOC=Corner,NAM=Video Rental Store,PC=2500,ROOM=Westerns and Classics,"
+			"PLC=store,POBOX=Private Box 15",
+		"retransmission-allowed='yes',ruleset-preference='https:/www/more.com',"
+			"retention-expires='2007-06-22T20:57:29Z'"
+		);
+	ast_test_validate(test, res == AST_TEST_PASS);
+
+
+	return res;
+}
+
+static void load_tests(void) {
+	AST_TEST_REGISTER(test_create_from_uri);
+	AST_TEST_REGISTER(test_create_from_pidf);
+}
+static void unload_tests(void) {
+	AST_TEST_UNREGISTER(test_create_from_uri);
+	AST_TEST_UNREGISTER(test_create_from_pidf);
+}
+
+#endif
diff --git a/res/res_geolocation/geoloc_gml.c b/res/res_geolocation/geoloc_gml.c
new file mode 100644
index 0000000..8a66162
--- /dev/null
+++ b/res/res_geolocation/geoloc_gml.c
@@ -0,0 +1,362 @@
+/*
+ * Copyright (C) 2022, Sangoma Technologies Corporation
+ *
+ * George Joseph <gjoseph at sangoma.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+#include "asterisk.h"
+#include "asterisk/config.h"
+#include "asterisk/cli.h"
+#include "asterisk/res_geolocation.h"
+#include "geoloc_private.h"
+
+
+#if 1 //not used yet.
+enum geoloc_shape_attrs {
+	GEOLOC_SHAPE_ATTR_POS = 0,
+	GEOLOC_SHAPE_ATTR_POS3D,
+	GEOLOC_SHAPE_ATTR_RADIUS,
+	GEOLOC_SHAPE_ATTR_SEMI_MAJOR_AXIS,
+	GEOLOC_SHAPE_ATTR_SEMI_MINOR_AXIS,
+	GEOLOC_SHAPE_ATTR_VERTICAL_AXIS,
+	GEOLOC_SHAPE_ATTR_HEIGHT,
+	GEOLOC_SHAPE_ATTR_ORIENTATION,
+	GEOLOC_SHAPE_ATTR_ORIENTATION_UOM,
+	GEOLOC_SHAPE_ATTR_INNER_RADIUS,
+	GEOLOC_SHAPE_ATTR_OUTER_RADIUS,
+	GEOLOC_SHAPE_ATTR_STARTING_ANGLE,
+	GEOLOC_SHAPE_ATTR_OPENING_ANGLE,
+	GEOLOC_SHAPE_ATTR_ANGLE_UOM,
+};
+
+struct geoloc_gml_attr_def {
+	enum geoloc_shape_attrs attr;
+	const char *name;
+	int (*validator)(const char *value);
+	int (*transformer)(struct ast_variable *value);
+};
+
+struct geoloc_gml_attr_def gml_attr_defs[] = {
+	{ GEOLOC_SHAPE_ATTR_POS, "pos", NULL, NULL},
+	{ GEOLOC_SHAPE_ATTR_POS3D,"pos3d", NULL, NULL},
+	{ GEOLOC_SHAPE_ATTR_RADIUS,"radius", NULL, NULL},
+	{ GEOLOC_SHAPE_ATTR_SEMI_MAJOR_AXIS,"semiMajorAxis", NULL, NULL},
+	{ GEOLOC_SHAPE_ATTR_SEMI_MINOR_AXIS,"semiMinorAxis", NULL, NULL},
+	{ GEOLOC_SHAPE_ATTR_VERTICAL_AXIS,"verticalAxis", NULL, NULL},
+	{ GEOLOC_SHAPE_ATTR_HEIGHT,"height", NULL, NULL},
+	{ GEOLOC_SHAPE_ATTR_ORIENTATION,"orientation", NULL, NULL},
+	{ GEOLOC_SHAPE_ATTR_ORIENTATION_UOM,"orientation_uom", NULL, NULL},
+	{ GEOLOC_SHAPE_ATTR_INNER_RADIUS,"innerRadius", NULL, NULL},
+	{ GEOLOC_SHAPE_ATTR_OUTER_RADIUS,"outerRadius", NULL, NULL},
+	{ GEOLOC_SHAPE_ATTR_STARTING_ANGLE,"startingAngle", NULL, NULL},
+	{ GEOLOC_SHAPE_ATTR_OPENING_ANGLE,"openingAngle", NULL, NULL},
+	{ GEOLOC_SHAPE_ATTR_ANGLE_UOM,"angle_uom", NULL, NULL},
+};
+#endif  //not used yet.
+
+struct geoloc_gml_attr {
+	const char *attribute;
+	int min_required;
+	int max_allowed;
+	int (*validator)(const char *value);
+};
+
+struct geoloc_gml_shape_def {
+	const char *shape_type;
+	struct geoloc_gml_attr required_attributes[8];
+};
+
+static int pos_validator(const char *value)
+{
+	float lat;
+	float lon;
+	return (sscanf(value, "%f %f", &lat, &lon) == 2);
+}
+
+static int pos3d_validator(const char *value)
+{
+	float lat;
+	float lon;
+	float alt;
+	return (sscanf(value, "%f %f %f", &lat, &lon, &alt) == 3);
+}
+
+static int float_validator(const char *value)
+{
+	float val;
+	return (sscanf(value, "%f", &val) == 1);
+}
+
+static int uom_validator(const char *value)
+{
+	return (ast_strings_equal(value, "degrees") || ast_strings_equal(value, "radians"));
+}
+
+
+static struct geoloc_gml_shape_def gml_shape_defs[8] = {
+	{ "Point", { {"pos", 1, 1, pos_validator}, {NULL, -1, -1} }},
+	{ "Polygon", { {"pos", 3, -1, pos_validator}, {NULL, -1, -1} }},
+	{ "Circle", { {"pos", 1, 1, pos_validator}, {"radius", 1, 1, float_validator},{NULL, -1, -1}}},
+	{ "Ellipse", { {"pos", 1, 1, pos_validator}, {"semiMajorAxis", 1, 1, float_validator},
+		{"semiMinorAxis", 1, 1, float_validator}, {"orientation", 1, 1, float_validator},
+		{"orientation_uom", 1, 1, uom_validator}, {NULL, -1, -1} }},
+	{ "ArcBand", { {"pos", 1, 1, pos_validator}, {"innerRadius", 1, 1, float_validator},
+		{"outerRadius", 1, 1, float_validator}, {"startAngle", 1, 1, float_validator},
+		{"startAngle_uom", 1, 1, uom_validator}, {"openingAngle", 1, 1, float_validator},
+		{"openingAngle_uom", 1, 1, uom_validator}, {NULL, -1, -1} }},
+	{ "Sphere", { {"pos3d", 1, 1, pos3d_validator}, {"radius", 1, 1, float_validator}, {NULL, -1, -1} }},
+	{ "Ellipse", { {"pos3d", 1, 1, pos3d_validator}, {"semiMajorAxis", 1, 1, float_validator},
+		{"semiMinorAxis", 1, 1, float_validator}, {"verticalAxis", 1, 1, float_validator},
+		{"orientation", 1, 1, float_validator}, {"orientation_uom", 1, 1, uom_validator}, {NULL, -1, -1} }},
+	{ "Prism", { {"pos3d", 3, -1, pos_validator}, {"height", 1, 1, float_validator}, {NULL, -1, -1} }},
+};
+
+enum ast_geoloc_validate_result ast_geoloc_gml_validate_varlist(const struct ast_variable *varlist,
+	const char **result)
+{
+	int def_index = -1;
+	const struct ast_variable *var;
+	int i;
+	const char *shape_type = ast_variable_find_in_list(varlist, "shape");
+
+	if (!shape_type) {
+		return AST_GEOLOC_VALIDATE_MISSING_SHAPE;
+	}
+
+	for (i = 0; i < ARRAY_LEN(gml_shape_defs); i++) {
+		if (ast_strings_equal(gml_shape_defs[i].shape_type, shape_type)) {
+			def_index = i;
+		}
+	}
+	if (def_index < 0) {
+		return AST_GEOLOC_VALIDATE_INVALID_SHAPE;
+	}
+
+	for (var = varlist; var; var = var->next) {
+		int vname_index = -1;
+		if (ast_strings_equal("shape", var->name)) {
+			continue;
+		}
+		for (i = 0; i < ARRAY_LEN(gml_shape_defs[def_index].required_attributes); i++) {
+			if (gml_shape_defs[def_index].required_attributes[i].attribute == NULL) {
+				break;
+			}
+			if (ast_strings_equal(gml_shape_defs[def_index].required_attributes[i].attribute, var->name)) {
+				vname_index = i;
+				break;
+			}
+		}
+		if (vname_index < 0) {
+			*result = var->name;
+			return AST_GEOLOC_VALIDATE_INVALID_VARNAME;
+		}
+		if (!gml_shape_defs[def_index].required_attributes[vname_index].validator(var->value)) {
+			*result = var->name;
+			return AST_GEOLOC_VALIDATE_INVALID_VALUE;
+		}
+	}
+
+	for (i = 0; i < ARRAY_LEN(gml_shape_defs[def_index].required_attributes); i++) {
+		int count = 0;
+		if (gml_shape_defs[def_index].required_attributes[i].attribute == NULL) {
+			break;
+		}
+
+		for (var = varlist; var; var = var->next) {
+			if (ast_strings_equal(gml_shape_defs[def_index].required_attributes[i].attribute, var->name)) {
+				count++;
+			}
+		}
+		if (count < gml_shape_defs[def_index].required_attributes[i].min_required) {
+			*result = gml_shape_defs[def_index].required_attributes[i].attribute;
+			return AST_GEOLOC_VALIDATE_NOT_ENOUGH_VARNAMES;
+		}
+		if (gml_shape_defs[def_index].required_attributes[i].max_allowed > 0 &&
+			count > gml_shape_defs[def_index].required_attributes[i].max_allowed) {
+			*result = gml_shape_defs[def_index].required_attributes[i].attribute;
+			return AST_GEOLOC_VALIDATE_TOO_MANY_VARNAMES;
+		}
+	}
+	return AST_GEOLOC_VALIDATE_SUCCESS;
+}
+
+static char *handle_gml_show(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
+{
+	int i;
+
+	switch (cmd) {
+	case CLI_INIT:
+		e->command = "geoloc show gml_shape_defs";
+		e->usage =
+			"Usage: geoloc show gml_shape_defs\n"
+			"       Show the GML Shape definitions.\n";
+		return NULL;
+	case CLI_GENERATE:
+		return NULL;
+	}
+
+	ast_cli(a->fd, "%-16s %-32s\n", "Shape", "Attributes name(min,max)");
+	ast_cli(a->fd, "================ ===============================\n");
+
+	for (i = 0; i < ARRAY_LEN(gml_shape_defs); i++) {
+		int j;
+		ast_cli(a->fd, "%-16s", gml_shape_defs[i].shape_type);
+		for (j = 0; j < ARRAY_LEN(gml_shape_defs[i].required_attributes); j++) {
+			if (gml_shape_defs[i].required_attributes[j].attribute == NULL) {
+				break;
+			}
+			if (gml_shape_defs[i].required_attributes[j].max_allowed >= 0) {
+				ast_cli(a->fd, " %s(%d,%d)", gml_shape_defs[i].required_attributes[j].attribute,
+					gml_shape_defs[i].required_attributes[j].min_required,
+					gml_shape_defs[i].required_attributes[j].max_allowed);
+			} else {
+				ast_cli(a->fd, " %s(%d,unl)", gml_shape_defs[i].required_attributes[j].attribute,
+					gml_shape_defs[i].required_attributes[j].min_required);
+			}
+		}
+		ast_cli(a->fd, "\n");
+	}
+	ast_cli(a->fd, "\n");
+
+	return CLI_SUCCESS;
+}
+
+static struct ast_cli_entry geoloc_gml_cli[] = {
+	AST_CLI_DEFINE(handle_gml_show, "Show the GML Shape definitions"),
+};
+
+struct ast_xml_node *geoloc_gml_list_to_xml(const struct ast_variable *resolved_location,
+	const char *ref_string)
+{
+	const char *shape;
+	char *crs;
+	struct ast_variable *var;
+	struct ast_xml_node *gml_node;
+	struct ast_xml_node *child_node;
+	int rc = 0;
+
+	SCOPE_ENTER(3, "%s", ref_string);
+
+	shape = ast_variable_find_in_list(resolved_location, "shape");
+	if (ast_strlen_zero(shape)) {
+		SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: There's no 'shape' parameter\n",
+			ref_string);
+	}
+	crs = (char *)ast_variable_find_in_list(resolved_location, "crs");
+	if (ast_strlen_zero(crs)) {
+		crs = "2d";
+	}
+
+	gml_node = ast_xml_new_node(shape);
+	if (!gml_node) {
+		SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create '%s' XML node\n", shape, ref_string);
+	}
+	rc = ast_xml_set_attribute(gml_node, "crs", crs);
+	if (rc != 0) {
+		ast_xml_free_node(gml_node);
+		SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create 'crs' XML attribute\n", ref_string);
+	}
+
+	for (var = (struct ast_variable *)resolved_location; var; var = var->next) {
+		RAII_VAR(char *, value, NULL, ast_free);
+		char *uom = NULL;
+
+		if (ast_strings_equal(var->name, "shape") || ast_strings_equal(var->name, "crs")) {
+			continue;
+		}
+		value = ast_strdup(var->value);
+
+		if (ast_strings_equal(var->name, "orientation") || ast_strings_equal(var->name, "startAngle")
+			|| ast_strings_equal(var->name, "openingAngle")) {
+			char *a = NULL;
+			char *junk = NULL;
+			float angle;
+			uom = value;
+
+			/* 'a' should now be the angle and 'uom' should be the uom */
+			a = strsep(&uom, " ");
+			angle = strtof(a, &junk);
+			/*
+			 * strtof sets junk to the first non-valid character so if it's
+			 * not empty after the conversion, there were unrecognized
+			 * characters in the angle.  It'll point to the NULL terminator
+			 * if angle was completely converted.
+			 */
+			if (!ast_strlen_zero(junk)) {
+				ast_xml_free_node(gml_node);
+				SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: The angle portion of parameter '%s' ('%s') is malformed\n",
+					ref_string, var->name, var->value);
+			}
+
+			if (ast_strlen_zero(uom)) {
+				uom = "degrees";
+			}
+
+			if (ast_begins_with(uom, "deg")) {
+				if (angle > 360.0) {
+					ast_xml_free_node(gml_node);
+					SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Parameter '%s': '%s' is malformed. "
+						"Degrees can't be > 360.0\n",
+						ref_string, var->name, var->value);
+				}
+			} else if (ast_begins_with(uom, "rad")) {
+				if(angle > 100.0) {
+					ast_xml_free_node(gml_node);
+					SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Parameter '%s': '%s' is malformed. "
+						"Radians can't be  > 100.0\n",
+						ref_string, var->name, var->value);
+				}
+			} else {
+				ast_xml_free_node(gml_node);
+				SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Parameter '%s': '%s' is malformed. "
+					"The unit of measure must be 'deg[rees]' or 'rad[ians]'\n",
+					ref_string, var->name, var->value);
+			}
+		}
+
+		child_node = ast_xml_new_child(gml_node, var->name);
+		if (!child_node) {
+			ast_xml_free_node(gml_node);
+			SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create '%s' XML node\n", var->name, ref_string);
+		}
+		if (!ast_strlen_zero(uom)) {
+			rc = ast_xml_set_attribute(child_node, "uom", uom);
+			if (rc != 0) {
+				ast_xml_free_node(gml_node);
+				SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create 'uom' XML attribute\n", ref_string);
+			}
+		}
+		ast_xml_set_text(child_node, value);
+	}
+
+	SCOPE_EXIT_RTN_VALUE(gml_node, "%s: Done\n", ref_string);
+}
+
+int geoloc_gml_unload(void)
+{
+	ast_cli_unregister_multiple(geoloc_gml_cli, ARRAY_LEN(geoloc_gml_cli));
+
+	return AST_MODULE_LOAD_SUCCESS;
+}
+
+int geoloc_gml_load(void)
+{
+	ast_cli_register_multiple(geoloc_gml_cli, ARRAY_LEN(geoloc_gml_cli));
+
+	return AST_MODULE_LOAD_SUCCESS;
+}
+
+int geoloc_gml_reload(void)
+{
+	return AST_MODULE_LOAD_SUCCESS;
+}
diff --git a/res/res_geolocation/geoloc_private.h b/res/res_geolocation/geoloc_private.h
new file mode 100644
index 0000000..860f3b5
--- /dev/null
+++ b/res/res_geolocation/geoloc_private.h
@@ -0,0 +1,168 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2022, Sangoma Technologies Corporation
+ *
+ * George Joseph <gjoseph at sangoma.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+#ifndef GEOLOC_PRIVATE_H_
+#define GEOLOC_PRIVATE_H_
+
+#include "asterisk/module.h"
+#include "asterisk/config.h"
+#include "asterisk/sorcery.h"
+#include "asterisk/lock.h"
+#include "asterisk/res_geolocation.h"
+
+#define CONFIG_STR_TO_ENUM_DECL(_stem) int geoloc_ ## _stem ## _str_to_enum(const char *str);
+CONFIG_STR_TO_ENUM_DECL(pidf_element)
+CONFIG_STR_TO_ENUM_DECL(format);
+CONFIG_STR_TO_ENUM_DECL(action);
+#define GEOLOC_ENUM_TO_NAME_DECL(_stem) const char * geoloc_ ## _stem ## _to_name(int ix);
+GEOLOC_ENUM_TO_NAME_DECL(pidf_element)
+GEOLOC_ENUM_TO_NAME_DECL(format);
+GEOLOC_ENUM_TO_NAME_DECL(action);
+
+
+#define CONFIG_STR_TO_ENUM(_stem) \
+int geoloc_ ## _stem ## _str_to_enum(const char *str) \
+{ \
+	int i; \
+	for (i = 0; i < ARRAY_LEN(_stem ## _names); i++) { \
+		if (ast_strings_equal(str, _stem ## _names[i])) { \
+			return i; \
+		} \
+	} \
+	return -1; \
+}
+
+#define CONFIG_ENUM_HANDLER(_object, _stem) \
+static int _stem ## _handler(const struct aco_option *opt, struct ast_variable *var, void *obj) \
+{ \
+	struct ast_geoloc_ ## _object *_thisobject = obj; \
+	int enumval = geoloc_ ## _stem ## _str_to_enum(var->value); \
+	if (enumval == -1) { \
+		return -1; \
+	} \
+	_thisobject->_stem = enumval; \
+	return 0; \
+}
+
+
+#define GEOLOC_ENUM_TO_NAME(_stem) \
+const char * geoloc_ ## _stem ## _to_name(int ix) \
+{ \
+	if (!ARRAY_IN_BOUNDS(ix, _stem ## _names)) { \
+		return "none"; \
+	} else { \
+		return _stem ## _names[ix]; \
+	} \
+}
+
+#define CONFIG_ENUM_TO_STR(_object, _stem) \
+static int _stem ## _to_str(const void *obj, const intptr_t *args, char **buf) \
+{ \
+	const struct ast_geoloc_ ## _object *_thisobject = obj; \
+	if (!ARRAY_IN_BOUNDS(_thisobject->_stem, _stem ## _names)) { \
+		*buf = ast_strdup("none"); \
+	} else { \
+		*buf = ast_strdup(_stem ## _names[_thisobject->_stem]); \
+	} \
+	return 0; \
+}
+
+#define CONFIG_ENUM(_object, _stem) \
+CONFIG_STR_TO_ENUM(_stem) \
+GEOLOC_ENUM_TO_NAME(_stem) \
+CONFIG_ENUM_HANDLER(_object, _stem) \
+CONFIG_ENUM_TO_STR(_object, _stem)
+
+#define CONFIG_VAR_LIST_HANDLER(_object, _stem) \
+static int _stem ## _handler(const struct aco_option *opt, struct ast_variable *var, void *obj) \
+{ \
+	struct ast_geoloc_ ## _object *_thisobject = obj; \
+	struct ast_variable *new_var; \
+	char *item_string, *item, *item_name, *item_value; \
+	int rc = 0;\
+	if (ast_strlen_zero(var->value)) { return 0; } \
+	item_string = ast_strdupa(var->value); \
+	while ((item = ast_strsep(&item_string, ',', AST_STRSEP_ALL))) { \
+		item_name = ast_strsep(&item, '=', AST_STRSEP_ALL); \
+		item_value = ast_strsep(&item, '=', AST_STRSEP_ALL); \
+		new_var = ast_variable_new(item_name, item_value, ""); \
+		if (!new_var) { \
+			rc = -1; \
+			break; \
+		} \
+		ast_variable_list_append(&_thisobject->_stem, new_var); \
+	} \
+	return rc; \
+}
+
+#define CONFIG_VAR_LIST_DUP(_object, _stem) \
+static int _stem ## _dup(const void *obj, struct ast_variable **fields) \
+{ \
+	const struct ast_geoloc_ ## _object *_thisobject = obj; \
+	if (_thisobject->_stem) { \
+		*fields = ast_variables_dup(_thisobject->_stem); \
+	} \
+	return 0; \
+}
+
+#define CONFIG_VAR_LIST_TO_STR(_object, _stem) \
+static int _stem ## _to_str(const void *obj, const intptr_t *args, char **buf) \
+{ \
+	const struct ast_geoloc_ ## _object *_thisobject = obj; \
+	struct ast_str *str = ast_variable_list_join(_thisobject->_stem, ",", "=", "\"", NULL); \
+	*buf = ast_strdup(ast_str_buffer(str)); \
+	ast_free(str); \
+	return 0; \
+}
+
+#define CONFIG_VAR_LIST(_object, _stem) \
+CONFIG_VAR_LIST_HANDLER(_object, _stem) \
+CONFIG_VAR_LIST_DUP(_object, _stem) \
+CONFIG_VAR_LIST_TO_STR(_object, _stem)
+
+int geoloc_config_load(void);
+int geoloc_config_reload(void);
+int geoloc_config_unload(void);
+
+struct ast_xml_node *geoloc_civicaddr_list_to_xml(const struct ast_variable *resolved_location,
+	const char *ref_string);
+int geoloc_civicaddr_load(void);
+int geoloc_civicaddr_unload(void);
+int geoloc_civicaddr_reload(void);
+
+struct ast_xml_node *geoloc_gml_list_to_xml(const struct ast_variable *resolved_location,
+	const char *ref_string);
+int geoloc_gml_unload(void);
+int geoloc_gml_load(void);
+int geoloc_gml_reload(void);
+
+int geoloc_dialplan_unload(void);
+int geoloc_dialplan_load(void);
+int geoloc_dialplan_reload(void);
+
+int geoloc_channel_unload(void);
+int geoloc_channel_load(void);
+int geoloc_channel_reload(void);
+
+int geoloc_eprofile_unload(void);
+int geoloc_eprofile_load(void);
+int geoloc_eprofile_reload(void);
+
+struct ast_sorcery *geoloc_get_sorcery(void);
+
+#endif /* GEOLOC_PRIVATE_H_ */
diff --git a/res/res_geolocation/pidf_lo_test.xml b/res/res_geolocation/pidf_lo_test.xml
new file mode 100644
index 0000000..3948063
--- /dev/null
+++ b/res/res_geolocation/pidf_lo_test.xml
@@ -0,0 +1,312 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<presence entity="pres:alice at asterisk.org"
+	xmlns="urn:ietf:params:xml:ns:pidf"
+	xmlns:ca="urn:ietf:params:xml:ns:pidf:geopriv10:civicAddr"
+	xmlns:dm="urn:ietf:params:xml:ns:pidf:data-model"
+	xmlns:gbp="urn:ietf:params:xml:ns:pidf:geopriv10:basicPolicy"
+	xmlns:gml="http://www.opengis.net/gml"
+	xmlns:gp="urn:ietf:params:xml:ns:pidf:geopriv10"
+	xmlns:gs="http://www.opengis.net/pidflo/1.0">
+	<tuple id="point-2d">
+		<status>
+			<gp:geopriv>
+				<gp:location-info>
+					<gml:Point srsName="urn:ogc:def:crs:EPSG::4326">
+						<gml:pos>-34.410649 150.87651</gml:pos>
+					</gml:Point>
+				</gp:location-info>
+				<gp:usage-rules>
+					<gbp:retransmission-allowed>no</gbp:retransmission-allowed>
+					<gbp:retention-expiry>2010-11-14T20:00:00Z</gbp:retention-expiry>
+				</gp:usage-rules>
+				<gp:method>Manual</gp:method>
+			</gp:geopriv>
+		</status>
+		<timestamp>2007-06-22T20:57:29Z</timestamp>
+	</tuple>
+	<dm:person id="point-3d">
+		<gp:geopriv>
+			<gp:location-info>
+				<gml:Point srsName="urn:ogc:def:crs:EPSG::4979">
+					<gml:pos>-34.410649 150.87651 1800</gml:pos>
+				</gml:Point>
+			</gp:location-info>
+			<gp:usage-rules>
+				<gbp:retransmission-allowed>no</gbp:retransmission-allowed>
+				<gbp:retention-expiry>2010-11-14T20:00:00Z</gbp:retention-expiry>
+			</gp:usage-rules>
+			<gp:method>802.11</gp:method>
+		</gp:geopriv>
+		<dm:timestamp>2007-06-24T12:28:04Z</dm:timestamp>
+	</dm:person>
+	<tuple id="circle-2d">
+		<status>
+			<gp:geopriv>
+				<gp:location-info>
+					<gs:Circle srsName="urn:ogc:def:crs:EPSG::4326">
+						<gml:pos>-34.410649 150.87651</gml:pos>
+						<gs:radius uom="urn:ogc:def:uom:EPSG::9001">30</gs:radius>
+					</gs:Circle>
+				</gp:location-info>
+				<gp:usage-rules>
+					<gbp:retransmission-allowed>no</gbp:retransmission-allowed>
+					<gbp:retention-expiry>2010-11-14T20:00:00Z</gbp:retention-expiry>
+				</gp:usage-rules>
+				<gp:method>802.11</gp:method>
+			</gp:geopriv>
+		</status>
+		<timestamp>2007-06-22T20:57:29Z</timestamp>
+	</tuple>
+	<tuple id="circle-3d">
+		<status>
+			<gp:geopriv>
+				<gp:location-info>
+					<gs:Circle srsName="urn:ogc:def:crs:EPSG::4979">
+						<gml:pos>-34.410649 150.87651 1800</gml:pos>
+						<gs:radius uom="urn:ogc:def:uom:EPSG::9001">30</gs:radius>
+					</gs:Circle>
+				</gp:location-info>
+				<gp:usage-rules>
+					<gbp:retransmission-allowed>no</gbp:retransmission-allowed>
+					<gbp:retention-expiry>2010-11-14T20:00:00Z</gbp:retention-expiry>
+				</gp:usage-rules>
+				<gp:method>802.11</gp:method>
+			</gp:geopriv>
+		</status>
+		<timestamp>2007-06-22T20:57:29Z</timestamp>
+	</tuple>
+	<dm:person id="polygon-2d">
+		<gp:geopriv>
+			<gp:location-info>
+				<gml:Polygon srsName="urn:ogc:def:crs:EPSG::4326">
+					<gml:exterior>
+						<gml:LinearRing>
+							<gml:pos>43.311 -73.422</gml:pos>
+							<gml:pos>43.111 -73.322</gml:pos>
+							<gml:pos>43.111 -73.222</gml:pos>
+							<gml:pos>43.311 -73.122</gml:pos>
+							<gml:pos>43.411 -73.222</gml:pos>
+							<gml:pos>43.411 -73.322</gml:pos>
+							<gml:pos>43.311 -73.422</gml:pos>
+						</gml:LinearRing>
+					</gml:exterior>
+				</gml:Polygon>
+			</gp:location-info>
+			<gp:usage-rules>
+				<gbp:retransmission-allowed>no</gbp:retransmission-allowed>
+				<gbp:retention-expiry>2010-11-14T20:00:00Z</gbp:retention-expiry>
+			</gp:usage-rules>
+			<gp:method>802.11</gp:method>
+		</gp:geopriv>
+		<dm:timestamp>2007-06-24T12:28:04Z</dm:timestamp>
+	</dm:person>
+	<dm:person id="polygon-3d">
+		<gp:geopriv>
+			<gp:location-info>
+				<gml:Polygon srsName="urn:ogc:def:crs:EPSG::4979">
+					<gml:exterior>
+						<gml:LinearRing>
+							<gml:pos>43.311 -73.422 1800</gml:pos>
+							<gml:pos>43.111 -73.322 1800</gml:pos>
+							<gml:pos>43.111 -73.222 1800</gml:pos>
+							<gml:pos>43.311 -73.122 1800</gml:pos>
+							<gml:pos>43.411 -73.222 1800</gml:pos>
+							<gml:pos>43.411 -73.322 1800</gml:pos>
+							<gml:pos>43.311 -73.422 1800</gml:pos>
+						</gml:LinearRing>
+					</gml:exterior>
+				</gml:Polygon>
+			</gp:location-info>
+			<gp:usage-rules>
+				<gbp:retransmission-allowed>no</gbp:retransmission-allowed>
+				<gbp:retention-expiry>2010-11-14T20:00:00Z</gbp:retention-expiry>
+			</gp:usage-rules>
+			<gp:method>802.11</gp:method>
+		</gp:geopriv>
+		<dm:timestamp>2007-06-24T12:28:04Z</dm:timestamp>
+	</dm:person>
+	<tuple id="polygon-poslist-2d">
+		<status>
+			<gp:geopriv>
+				<gp:location-info>
+					<gml:Polygon srsName="urn:ogc:def:crs:EPSG::4326">
+						<gml:exterior>
+							<gml:LinearRing>
+								<gml:posList>
+									43.311 -73.422 43.111 -73.322
+									43.111 -73.222 43.311 -73.122
+									43.411 -73.222 43.411 -73.322
+									43.311 -73.422
+								</gml:posList>
+							</gml:LinearRing>
+						</gml:exterior>
+					</gml:Polygon>
+				</gp:location-info>
+				<gp:usage-rules>
+					<gbp:retransmission-allowed>no</gbp:retransmission-allowed>
+					<gbp:retention-expiry>2010-11-14T20:00:00Z</gbp:retention-expiry>
+				</gp:usage-rules>
+				<gp:method>Wiremap</gp:method>
+			</gp:geopriv>
+		</status>
+		<timestamp>2007-06-22T20:57:29Z</timestamp>
+	</tuple>
+	<tuple id="polygon-poslist-3d">
+		<status>
+			<gp:geopriv>
+				<gp:location-info>
+					<gml:Polygon srsName="urn:ogc:def:crs:EPSG::4979">
+						<gml:exterior>
+							<gml:LinearRing>
+								<gml:posList>
+									43.311 -73.422 1800 43.111 -73.322 1800
+									43.111 -73.222 1800 43.311 -73.122 1800
+									43.411 -73.222 1800 43.411 -73.322 1800
+									43.311 -73.422 1800
+								</gml:posList>
+							</gml:LinearRing>
+						</gml:exterior>
+					</gml:Polygon>
+				</gp:location-info>
+				<gp:usage-rules>
+					<gbp:retransmission-allowed>no</gbp:retransmission-allowed>
+					<gbp:retention-expiry>2010-11-14T20:00:00Z</gbp:retention-expiry>
+				</gp:usage-rules>
+				<gp:method>Wiremap</gp:method>
+			</gp:geopriv>
+		</status>
+		<timestamp>2007-06-22T20:57:29Z</timestamp>
+	</tuple>
+	<tuple id="ellipse-2d">
+		<status>
+			<gp:geopriv>
+				<gp:location-info>
+					<gs:Ellipse srsName="urn:ogc:def:crs:EPSG::4326">
+						<gml:pos>42.5463 -73.2512</gml:pos>
+						<gs:semiMajorAxis uom="urn:ogc:def:uom:EPSG::9001">1275</gs:semiMajorAxis>
+						<gs:semiMinorAxis uom="urn:ogc:def:uom:EPSG::9001">670</gs:semiMinorAxis>
+						<gs:orientation uom="urn:ogc:def:uom:EPSG::9102">43.2</gs:orientation>
+					</gs:Ellipse>
+				</gp:location-info>
+				<gp:usage-rules/>
+				<gp:method>Device-Assisted_A-GPS</gp:method>
+			</gp:geopriv>
+		</status>
+		<timestamp>2007-06-22T20:57:29Z</timestamp>
+	</tuple>
+	<dm:device id="arcband-2d">
+		<gp:geopriv>
+			<gp:location-info>
+				<gs:ArcBand srsName="urn:ogc:def:crs:EPSG::4326">
+					<gml:pos>-43.5723 153.21760</gml:pos>
+					<gs:innerRadius uom="urn:ogc:def:uom:EPSG::9001">3594</gs:innerRadius>
+					<gs:outerRadius uom="urn:ogc:def:uom:EPSG::9001">4148</gs:outerRadius>
+					<gs:startAngle uom="urn:ogc:def:uom:EPSG::9102">20</gs:startAngle>
+					<gs:openingAngle uom="urn:ogc:def:uom:EPSG::9102">20</gs:openingAngle>
+				</gs:ArcBand>
+			</gp:location-info>
+			<gp:usage-rules>
+				<gp:retransmission-allowed>yes</gp:retransmission-allowed>
+				<gp:ruleset-preference>https:/www/more.com</gp:ruleset-preference>
+				<gp:retention-expires>2007-06-22T20:57:29Z</gp:retention-expires>
+			</gp:usage-rules>
+			<gp:method>TA-NMR</gp:method>
+		</gp:geopriv>
+		<dm:deviceID>mac:1234567890ab</dm:deviceID>
+		<dm:timestamp>2007-06-22T20:57:29Z</dm:timestamp>
+	</dm:device>
+	<tuple id="sphere">
+		<status>
+			<gp:geopriv>
+				<gp:location-info>
+					<gs:Sphere srsName="urn:ogc:def:crs:EPSG::4979">
+						<gml:pos>42.5463 -73.2512 26.3</gml:pos>
+						<gs:radius uom="urn:ogc:def:uom:EPSG::9001">850.24</gs:radius>
+					</gs:Sphere>
+				</gp:location-info>
+				<gp:usage-rules>
+					<gbp:retransmission-allowed>no</gbp:retransmission-allowed>
+					<gbp:retention-expiry>2010-11-14T20:00:00Z</gbp:retention-expiry>
+				</gp:usage-rules>
+				<gp:method>Device-Based_A-GPS</gp:method>
+			</gp:geopriv>
+		</status>
+	</tuple>
+	<tuple id="ellipsoid">
+		<status>
+			<gp:geopriv>
+				<gp:location-info>
+					<gs:Ellipsoid srsName="urn:ogc:def:crs:EPSG::4979">
+						<gml:pos>42.5463 -73.2512 26.3</gml:pos>
+						<gs:semiMajorAxis uom="urn:ogc:def:uom:EPSG::9001">7.7156</gs:semiMajorAxis>
+						<gs:semiMinorAxis uom="urn:ogc:def:uom:EPSG::9001">3.31</gs:semiMinorAxis>
+						<gs:verticalAxis uom="urn:ogc:def:uom:EPSG::9001">28.7</gs:verticalAxis>
+						<gs:orientation uom="urn:ogc:def:uom:EPSG::9102">90</gs:orientation>
+					</gs:Ellipsoid>
+				</gp:location-info>
+				<gp:usage-rules/>
+				<gp:method>Hybrid_A-GPS</gp:method>
+			</gp:geopriv>
+		</status>
+		<timestamp>2007-06-22T20:57:29Z</timestamp>
+	</tuple>
+	<tuple id="prism">
+		<status>
+			<gp:geopriv>
+				<gp:location-info>
+					<gs:Prism srsName="urn:ogc:def:crs:EPSG::4979">
+						<gs:base>
+							<gml:Polygon>
+								<gml:exterior>
+									<gml:LinearRing>
+										<gml:posList>
+											42.556844 -73.248157 36.6 <!--A -->
+											42.656844 -73.248157 36.6 <!--B -->
+											42.656844 -73.348157 36.6 <!--C -->
+											42.556844 -73.348157 36.6 <!--D -->
+											42.556844 -73.248157 36.6 <!--A -->
+										</gml:posList>
+									</gml:LinearRing>
+								</gml:exterior>
+							</gml:Polygon>
+						</gs:base>
+						<gs:height uom="urn:ogc:def:uom:EPSG::9001">2.4</gs:height>
+					</gs:Prism>
+				</gp:location-info>
+				<gp:usage-rules/>
+				<gp:method>Wiremap</gp:method>
+			</gp:geopriv>
+		</status>
+		<timestamp>2007-06-22T20:57:29Z</timestamp>
+	</tuple>
+	<dm:device>
+		<gp:geopriv>
+			<gp:location-info>
+				<ca:civicAddress xml:lang="en-AU">
+					<ca:country>AU</ca:country>
+					<ca:A1>NSW</ca:A1>
+					<ca:A3>Wollongong</ca:A3>
+					<ca:A4>North Wollongong</ca:A4>
+					<ca:RD>Flinders</ca:RD>
+					<ca:STS>Street</ca:STS>
+					<ca:RDBR>Campbell Street</ca:RDBR>
+					<ca:LMK>Gilligan's Island</ca:LMK>
+					<ca:LOC>Corner</ca:LOC>
+					<ca:NAM> Video Rental Store </ca:NAM>
+					<ca:PC>2500</ca:PC>
+					<ca:ROOM> Westerns and Classics </ca:ROOM>
+					<ca:PLC>store</ca:PLC>
+					<ca:POBOX>Private Box 15</ca:POBOX>
+				</ca:civicAddress>
+			</gp:location-info>
+			<gp:usage-rules>
+				<gp:retransmission-allowed>yes</gp:retransmission-allowed>
+				<gp:ruleset-preference>https:/www/more.com</gp:ruleset-preference>
+				<gp:retention-expires>2007-06-22T20:57:29Z</gp:retention-expires>
+			</gp:usage-rules>
+			<gp:method>GPS</gp:method>
+		</gp:geopriv>
+		<dm:deviceID>mac:1234567890ab</dm:deviceID>
+		<dm:timestamp>2007-06-22T20:57:29Z</dm:timestamp>
+	</dm:device>
+</presence>
diff --git a/res/res_geolocation/pidf_to_eprofile.xslt b/res/res_geolocation/pidf_to_eprofile.xslt
new file mode 100644
index 0000000..05f4df8
--- /dev/null
+++ b/res/res_geolocation/pidf_to_eprofile.xslt
@@ -0,0 +1,212 @@
+<?xml version="1.0"?>
+<xsl:stylesheet version="1.0"
+	xmlns:ca="urn:ietf:params:xml:ns:pidf:geopriv10:civicAddr"
+	xmlns:def="urn:ietf:params:xml:ns:pidf"
+	xmlns:dm="urn:ietf:params:xml:ns:pidf:data-model"
+	xmlns:fn="http://www.w3.org/2005/xpath-functions"
+	xmlns:gbp="urn:ietf:params:xml:ns:pidf:geopriv10:basicPolicy"
+	xmlns:gml="http://www.opengis.net/gml"
+	xmlns:gp="urn:ietf:params:xml:ns:pidf:geopriv10"
+	xmlns:gs="http://www.opengis.net/pidflo/1.0"
+	xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+
+
+<!--
+	The whole purpose of this stylesheet is to convert a PIDF-LO document into a simple,
+	common XML document that is easily parsable by geoloc_eprofile into an eprofile.
+
+	For example:
+
+	<presence>
+		<device>
+			<location-info format="GML">shape="Point", crs="2d", pos="38.456 -105.678"</location-info>
+			<usage-rules>retransmission-allowed=no</usage-rules>
+			<method>GPS</method>
+		</device>
+	</presence>
+
+	WARNING:  Don't mess with this stylesheet before brushing up your
+	XPath and XSLT expertise.
+-->
+
+
+<!--
+	All of the namespaces that could be in the incoming PIDF-LO document
+	have to be declared above.  All matching is done based on the URI, not
+	the prefix so we can use whatever prefixes we want.  For instance,
+	even if "urn:ietf:params:xml:ns:pidf:data-model" were declared with
+	the "pdm" prefix in the incoming document and with "dm" here,
+	"dm:device" would match "pdm:device" in the document.
+-->
+
+	<xsl:output method="xml" indent="yes"/>
+	<xsl:strip-space elements="*"/>
+	<xsl:param name="path"/>
+
+	<!--
+		Even though the "presence", "tuple", and "status" elements won't have namespaces in the
+		incoming PIDF document, we have to use the pseudo-namespace "def" here because of namespace
+		processing quirks in libxml2 and libxslt.
+
+		We don't use namespace prefixes in the output document at all.
+	-->
+	<xsl:template match="/def:presence">
+		<xsl:element name="presence">
+			<xsl:attribute name="entity"><xsl:value-of select="@entity"/></xsl:attribute>
+			<xsl:apply-templates select="$path"/>
+		</xsl:element>
+	</xsl:template>
+
+	<xsl:template match="dm:device">
+		<xsl:element name="device">
+			<xsl:attribute name="id"><xsl:value-of select="@id"/></xsl:attribute>
+			<xsl:apply-templates select=".//gp:location-info"/>
+			<xsl:apply-templates select=".//gp:usage-rules"/>
+			<xsl:apply-templates select=".//gp:method"/>
+			<xsl:apply-templates select=".//gp:note-well"/>
+			<xsl:if test="./dm:timestamp">
+				<timestamp>
+					<xsl:value-of select="./dm:timestamp"/>
+				</timestamp>
+			</xsl:if>
+			<xsl:if test="./dm:deviceID">
+				<deviceID>
+					<xsl:value-of select="./dm:deviceID"/>
+				</deviceID>
+			</xsl:if>
+		</xsl:element>
+	</xsl:template>
+
+	<xsl:template match="def:tuple">
+		<xsl:element name="tuple">
+			<xsl:attribute name="id"><xsl:value-of select="@id"/></xsl:attribute>
+			<xsl:apply-templates select=".//gp:location-info"/>
+			<xsl:apply-templates select=".//gp:usage-rules"/>
+			<xsl:apply-templates select=".//gp:method"/>
+			<xsl:apply-templates select=".//gp:note-well"/>
+			<xsl:if test="./timestamp">
+				<timestamp>
+					<xsl:value-of select="./timestamp"/>
+				</timestamp>
+			</xsl:if>
+		</xsl:element>
+	</xsl:template>
+
+	<xsl:template match="dm:person">
+		<xsl:element name="person">
+			<xsl:attribute name="id"><xsl:value-of select="@id"/></xsl:attribute>
+			<xsl:apply-templates select=".//gp:location-info"/>
+			<xsl:apply-templates select=".//gp:usage-rules"/>
+			<xsl:apply-templates select=".//gp:method"/>
+			<xsl:apply-templates select=".//gp:note-well"/>
+			<xsl:if test="./dm:timestamp">
+				<timestamp>
+					<xsl:value-of select="./dm:timestamp"/>
+				</timestamp>
+			</xsl:if>
+		</xsl:element>
+	</xsl:template>
+
+	<xsl:template match="gp:location-info/gml:*">
+		<xsl:element name="location-info">
+			<xsl:attribute name="format">gml</xsl:attribute>
+			<xsl:call-template name="shape" />
+		</xsl:element>
+	</xsl:template>
+
+	<xsl:template match="gp:location-info/gs:*">
+		<xsl:element name="location-info">
+			<xsl:attribute name="format">gml</xsl:attribute>
+			<xsl:call-template name="shape" />
+		</xsl:element>
+	</xsl:template>
+
+	<xsl:template match="gp:location-info/ca:civicAddress">
+		<xsl:element name="location-info">
+			<xsl:attribute name="format">civicAddress</xsl:attribute>
+			<xsl:call-template name="civicAddress" />
+		</xsl:element>
+	</xsl:template>
+
+	<!--
+		All of the "following-sibling" things just stick a comma after the value if there's another
+		element after it.  The result should be...
+
+		name1="value1", name2="value2"
+	-->
+	<xsl:template name="name-value">
+		<xsl:element name="{local-name(.)}">
+			<xsl:value-of select="normalize-space(.)"/>
+		</xsl:element>
+	</xsl:template>
+
+	<xsl:template name="length"><xsl:call-template name="name-value" /></xsl:template>
+
+	<xsl:template name="angle">
+		<xsl:element name="{local-name(.)}">
+			<xsl:choose>
+				<xsl:when test="@uom = 'urn:ogc:def:uom:EPSG::9102'">
+					<xsl:attribute name="uom">radians</xsl:attribute></xsl:when>
+				<xsl:otherwise>
+					<xsl:attribute name="uom">degrees</xsl:attribute></xsl:otherwise>
+			</xsl:choose>
+			<xsl:value-of select="normalize-space(.)"/>
+		</xsl:element>
+	</xsl:template>
+
+	<xsl:template match="gs:orientation"><xsl:call-template name="angle" /></xsl:template>
+	<xsl:template match="gs:radius"><xsl:call-template name="length" /></xsl:template>
+	<xsl:template match="gs:height"><xsl:call-template name="length" /></xsl:template>
+	<xsl:template match="gs:semiMajorAxis"><xsl:call-template name="length" /></xsl:template>
+	<xsl:template match="gs:semiMinorAxis"><xsl:call-template name="length" /></xsl:template>
+	<xsl:template match="gs:verticalAxis"><xsl:call-template name="length" /></xsl:template>
+	<xsl:template match="gs:innerRadius"><xsl:call-template name="length" /></xsl:template>
+	<xsl:template match="gs:outerRadius"><xsl:call-template name="length" /></xsl:template>
+	<xsl:template match="gs:startAngle"><xsl:call-template name="angle" /></xsl:template>
+	<xsl:template match="gs:openingAngle"><xsl:call-template name="angle" /></xsl:template>
+	<xsl:template match="gml:pos"><xsl:call-template name="name-value" /></xsl:template>
+	<xsl:template match="gml:posList"><xsl:call-template name="name-value" /></xsl:template>
+
+	<xsl:template name="shape">
+		<xsl:element name="{local-name(.)}">
+			<xsl:choose>
+			<xsl:when test="@srsName = 'urn:ogc:def:crs:EPSG::4326'">
+				<xsl:attribute name="srsName">2d</xsl:attribute>
+			</xsl:when>
+			<xsl:when test="@srsName = 'urn:ogc:def:crs:EPSG::4979'">
+				<xsl:attribute name="srsName">3d</xsl:attribute>
+			</xsl:when>
+			<xsl:otherwise>
+				<xsl:attribute name="srsName">unknown</xsl:attribute>
+			</xsl:otherwise>
+			</xsl:choose>
+			<xsl:apply-templates />
+		</xsl:element>
+	</xsl:template>
+
+	<xsl:template match="ca:civicAddress/*"><xsl:call-template name="name-value" /></xsl:template>
+	<xsl:template name="civicAddress">
+		<xsl:element name="{local-name(.)}">
+			<xsl:attribute name="lang"><xsl:value-of select="@xml:lang"/></xsl:attribute>
+			<xsl:apply-templates select="./*"/>
+		</xsl:element>
+	</xsl:template>
+
+	<xsl:template match="gp:usage-rules/*">
+		<xsl:call-template name="name-value" />
+	</xsl:template>
+
+	<xsl:template match="gp:usage-rules">
+		<xsl:element name="usage-rules">
+			<xsl:apply-templates />
+		</xsl:element>
+	</xsl:template>
+
+	<xsl:template match="gp:method">
+		<xsl:element name="method">
+		<xsl:value-of select="normalize-space(.)" />
+		</xsl:element>
+	</xsl:template>
+
+
+</xsl:stylesheet>

-- 
To view, visit https://gerrit.asterisk.org/c/asterisk/+/18797
To unsubscribe, or for help writing mail filters, visit https://gerrit.asterisk.org/settings

Gerrit-Project: asterisk
Gerrit-Branch: certified/18.9
Gerrit-Change-Id: Ibfde963121b1ecf57fd98ee7060c4f0808416303
Gerrit-Change-Number: 18797
Gerrit-PatchSet: 1
Gerrit-Owner: George Joseph <gjoseph at digium.com>
Gerrit-Reviewer: George Joseph <gjoseph at digium.com>
Gerrit-Reviewer: Kevin Harwell <kharwell at digium.com>
Gerrit-CC: Friendly Automation
Gerrit-MessageType: merged
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.digium.com/pipermail/asterisk-code-review/attachments/20220713/2907a614/attachment-0001.html>


More information about the asterisk-code-review mailing list